Skip to content

Latest commit

 

History

History
29 lines (23 loc) · 10.1 KB

File metadata and controls

29 lines (23 loc) · 10.1 KB

openserver

  • Zod v4 breaking change: z.record() requires two arguments. In Zod v3, z.record(z.string()) validated a Record<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(), not server.server.sendNotification(). When dynamic tool registration changes the tool list, the correct McpServer method is server.sendToolListChanged(). Calling the lower-level server.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), use import(\${filePath}?t=${Date.now()}`)`. Without the query string, the cached module is returned even if the file changed on disk.

  • Bun's fs.watch supports { recursive: true } natively on all platforms. Unlike Node's fs.watch, which silently degrades on Linux, Bun's implementation supports the recursive option consistently. No need for platform guards or chokidar for 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 with matter(raw), mutate file.data, then write back with matter.stringify(file.content, file.data). First arg is the body (without frontmatter), second is the fields object. Avoids hand-rolling YAML serialization.

  • registerAllCollections / registerAllRoutes silently skip child schemas. Both auto-mcp.ts and auto-api.ts call resolveDataDir(schema) without a parentSlug. For schemas that declare a parent, resolveDataDir throws, the catch block logs a warning, and the schema is skipped. Child schemas only get tools/routes when registerCollectionTools / registerCollectionRoutes is called explicitly with a concrete parentSlug. Calling the bulk-registration helpers after defining a parent+child pair will leave the child silently toolless.

  • Startup schema loader in meta-tools/schemas.ts has the same parent-blindness. The IIFE that reloads persisted schemas derives dataDir as path.join(projectRoot, "data", "${name}s") — no parent awareness. Any child schema saved to disk will have its tools registered pointing at a flat data/<childName>s/ path rather than data/<parentSlug>/<childName>s/. The data will never be found and no error is thrown.

  • Naive +s pluralization is hardcoded throughout every layer. resolveDataDir (schema-engine.ts), the MCP tool name list_${name}s, the HTTP routes /api/${name}s, and the startup loader all unconditionally append s to the schema name. Schemas with names ending in s, x, z, ch, or sh, or with irregular plurals, will produce broken paths and tool names silently.

  • SchemaArg union in fs-db.ts allows raw ZodObject for backward compatibility. type SchemaArg = ResolvedSchema | z.ZodObject<any> lets callers pass either a ResolvedSchema (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 raw ZodObject silently bypasses any ResolvedSchema metadata (e.g., parent, fields) that fs-db functions 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 dataDir correctly. A single-pass loop over getAllSchemas() 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 where schema.parent is 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 at src/server.ts needs "../" (one level) to reach the project root, and a file at src/meta-tools/foo.ts needs "../../" (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 await before routes register. schemas.ts runs an async IIFE that loads persisted schemas and registers their tools, but nothing awaits it. If registerAllRoutes() is called synchronously after importing schemas.ts, the route map is built against an empty registry and all API routes are silently absent. The fix is a dedicated schema-loader.ts that exports a top-level await of 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 to Bun.serve({ routes }). A 2-segment pattern like /api/posts/:id will shadow a 4-segment /api/posts/:parentId/comments/:id if 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 to Bun.serve.

  • WebStandardStreamableHTTPServerTransport is stateful — one instance cannot serve multiple MCP client sessions. When transport: "http", the SDK creates a single WebStandardStreamableHTTPServerTransport instance shared for the server's lifetime. A second initialize request 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.

2-create-server

  • setDataDirPrefix is module-level global state — calling createServer twice with different dataDir values corrupts both instances. schema-engine.ts stores the prefix in a module-level let dataDirPrefix. createServer calls setDataDirPrefix(dataDir) at construction time, not inside start(). A second createServer({ dataDir: "other" }) call overwrites the prefix for the first instance. Likewise, test suites that spin up multiple servers with different dataDir values will interfere. The fix is either to guard against multiple instantiations or to pass dataDir as a parameter to resolveDataDir instead of storing it globally.
  • schemas: ResolvedSchema[] in CreateServerOptions is decorative — schemas are registered via side-effect imports, not via the parameter. createServer logs each schema in the array but never passes it to registerAllCollections or schemaRegistry. registerAllCollections(mcpServer) reads directly from schemaRegistry, which is populated only when schema files are imported (calling defineSchema). A caller that passes schemas without importing the schema files gets zero registered tools; a caller that imports schema files gets all tools regardless of what they pass in schemas. The parameter gives a false impression of declarative registration.

1-package-npm

  • bun build --target bun produces 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 in package.json's peerDependencies/dependencies and passed via --external flags, 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-lib worktree, it applied edits to template/ (the path on master) rather than packages/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 run ls to verify before any file writes.
  • bun-types must be added as an explicit devDependency for tsc to resolve Bun globals. Running tsc in a project that uses Bun-specific APIs (Bun.serve, import.meta.url, etc.) fails with "cannot find name 'Bun'" unless @types/bun (or the bun-types package) is listed in devDependencies and referenced in tsconfig.json via compilerOptions.types. Bun's own type definitions are not automatically injected when TypeScript is invoked directly.
  • Copying bin/ and template/ 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 root bin/ and template/ into packages/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 use git mv to make the intent explicit in history.