-
Zod v4 breaking change:
z.record()requires two arguments. In Zod v3,z.record(z.string())validated aRecord<string, string>. In Zod v4, the single-argument form is a type error — you must pass both key and value schemas:z.record(z.string(), z.string()). Affects any schema that maps arbitrary string keys to typed values. -
McpServer notification API: use
sendToolListChanged(), notserver.server.sendNotification(). When dynamic tool registration changes the tool list, the correct McpServer method isserver.sendToolListChanged(). Calling the lower-levelserver.server.sendNotification("notifications/tools/list_changed", {})bypasses the abstraction and may not reach clients correctly. -
Dynamic
import()cache busting: append?t=Date.now()to the module path. Node/Bun cache ES module imports by resolved path. To force a re-import of a changed file at runtime (e.g., hot-reloading plugin modules), useimport(\${filePath}?t=${Date.now()}`)`. Without the query string, the cached module is returned even if the file changed on disk. -
Bun's
fs.watchsupports{ recursive: true }natively on all platforms. Unlike Node'sfs.watch, which silently degrades on Linux, Bun's implementation supports therecursiveoption consistently. No need for platform guards orchokidarfor deep directory watching when running under Bun. -
gray-matter.stringify(body, fields)for round-trip frontmatter writes. To update frontmatter without parsing the full file manually: read withmatter(raw), mutatefile.data, then write back withmatter.stringify(file.content, file.data). First arg is the body (without frontmatter), second is the fields object. Avoids hand-rolling YAML serialization. -
registerAllCollections/registerAllRoutessilently skip child schemas. Bothauto-mcp.tsandauto-api.tscallresolveDataDir(schema)without aparentSlug. For schemas that declare aparent,resolveDataDirthrows, the catch block logs a warning, and the schema is skipped. Child schemas only get tools/routes whenregisterCollectionTools/registerCollectionRoutesis called explicitly with a concreteparentSlug. Calling the bulk-registration helpers after defining a parent+child pair will leave the child silently toolless. -
Startup schema loader in
meta-tools/schemas.tshas the same parent-blindness. The IIFE that reloads persisted schemas derivesdataDiraspath.join(projectRoot, "data", "${name}s")— no parent awareness. Any child schema saved to disk will have its tools registered pointing at a flatdata/<childName>s/path rather thandata/<parentSlug>/<childName>s/. The data will never be found and no error is thrown. -
Naive
+spluralization is hardcoded throughout every layer.resolveDataDir(schema-engine.ts), the MCP tool namelist_${name}s, the HTTP routes/api/${name}s, and the startup loader all unconditionally appendsto the schema name. Schemas with names ending ins,x,z,ch, orsh, or with irregular plurals, will produce broken paths and tool names silently. -
SchemaArgunion infs-db.tsallows rawZodObjectfor backward compatibility.type SchemaArg = ResolvedSchema | z.ZodObject<any>lets callers pass either aResolvedSchema(from the registry) or a bare Zod schema (old call pattern). The discriminator is"zodSchema" in schema. Both types are structurally similar at call sites; passing a rawZodObjectsilently bypasses anyResolvedSchemametadata (e.g.,parent,fields) thatfs-dbfunctions don't use directly but callers may expect to be validated. -
Two-pass registration (roots first, children second) is required for parent-child schema auto-registration. When bulk-registering schemas at startup, child schemas reference parent schemas that must already be in the registry to resolve
dataDircorrectly. A single-pass loop overgetAllSchemas()will fail for any child whose parent appears later in iteration order. The fix: first pass registers all schemas where!schema.parent, second pass registers all whereschema.parentis set. This guarantees the parent registry entry exists before any child tries to resolve against it. -
new URL("../...", import.meta.url)removes the filename before counting..segments.new URL("../..", "file:///a/b/c/d.ts")resolves to/a/, not/a/b/. The URL API treats the base as a directory only when it ends in/; otherwise the last segment is treated as a file and stripped before any traversal. Concretely: a file atsrc/server.tsneeds"../"(one level) to reach the project root, and a file atsrc/meta-tools/foo.tsneeds"../../"(two levels) — not three. Using one extra..silently resolves to the parent of the project root, making all file I/O target the wrong directory with no error. -
Schema startup IIFE is fire-and-forget — schemas must be loaded with
awaitbefore routes register.schemas.tsruns an async IIFE that loads persisted schemas and registers their tools, but nothingawaits it. IfregisterAllRoutes()is called synchronously afterimportingschemas.ts, the route map is built against an empty registry and all API routes are silently absent. The fix is a dedicatedschema-loader.tsthat exports a top-levelawaitof the load function, imported before any route registration, so the module system guarantees ordering. -
Bun.serve route matching is order-sensitive — deeper paths must be registered before shallower ones. In
auto-api.ts, all routes are collected into an array and passed toBun.serve({ routes }). A 2-segment pattern like/api/posts/:idwill shadow a 4-segment/api/posts/:parentId/comments/:idif registered first, because Bun matches routes in declaration order and stops at the first match. Deeper (more-specific) routes must appear earlier in the array than their shallower ancestors. The safe pattern: sort routes by segment count descending before passing toBun.serve. -
WebStandardStreamableHTTPServerTransportis stateful — one instance cannot serve multiple MCP client sessions. Whentransport: "http", the SDK creates a singleWebStandardStreamableHTTPServerTransportinstance shared for the server's lifetime. A secondinitializerequest from a different client session on the same transport is rejected. To support multiple concurrent MCP clients, either instantiate a new transport per session or use a stateless transport (sessionIdGenerator: undefined). Contract tests for the HTTP transport must share a single client across all test cases rather than creating a fresh client per test.
setDataDirPrefixis module-level global state — callingcreateServertwice with differentdataDirvalues corrupts both instances.schema-engine.tsstores the prefix in a module-levellet dataDirPrefix.createServercallssetDataDirPrefix(dataDir)at construction time, not insidestart(). A secondcreateServer({ dataDir: "other" })call overwrites the prefix for the first instance. Likewise, test suites that spin up multiple servers with differentdataDirvalues will interfere. The fix is either to guard against multiple instantiations or to passdataDiras a parameter toresolveDataDirinstead of storing it globally.schemas: ResolvedSchema[]inCreateServerOptionsis decorative — schemas are registered via side-effect imports, not via the parameter.createServerlogs each schema in the array but never passes it toregisterAllCollectionsorschemaRegistry.registerAllCollections(mcpServer)reads directly fromschemaRegistry, which is populated only when schema files are imported (callingdefineSchema). A caller that passesschemaswithout importing the schema files gets zero registered tools; a caller that imports schema files gets all tools regardless of what they pass inschemas. The parameter gives a false impression of declarative registration.
bun build --target bunproduces large self-contained bundles (~650 KB per entry point) by inlining all dependencies. This is expected behavior for the Bun target: the bundler resolves and embeds every imported module, including third-party packages, into a single output file. For a library distributed on npm this is usually undesirable — consumers end up with duplicate copies of shared deps. The correct approach for a publishable lib is to use--target node(or omit--target) with external dependencies listed inpackage.json'speerDependencies/dependenciesand passed via--externalflags, so bundlers used by downstream consumers can deduplicate them.- Worktree agents operating on a feature branch will write to paths that exist on that branch, not on master — diverged path layouts silently misdirect all file I/O. When a sub-agent was spawned inside the
feat/openserver-libworktree, it applied edits totemplate/(the path on master) rather thanpackages/create-openserver/template/(the restructured path on the feature branch). The agent had no awareness that the directory tree had changed. Mitigations: explicitly state the expected layout in agent instructions, or have the agent runlsto verify before any file writes. bun-typesmust be added as an explicit devDependency fortscto resolve Bun globals. Runningtscin a project that uses Bun-specific APIs (Bun.serve,import.meta.url, etc.) fails with "cannot find name 'Bun'" unless@types/bun(or thebun-typespackage) is listed indevDependenciesand referenced intsconfig.jsonviacompilerOptions.types. Bun's own type definitions are not automatically injected when TypeScript is invoked directly.- Copying
bin/andtemplate/to a new monorepo path without deleting the originals leaves duplicate artifacts at the repo root. During monorepo restructuring, the scaffolder assets were duplicated from rootbin/andtemplate/intopackages/create-openserver/. The originals were not removed. This causes confusion about which copy is canonical and risks divergence if either copy is edited independently. Always delete the source after a structural move, or usegit mvto make the intent explicit in history.