@contentrain/mcp 3.6.8 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/dist/{annotations-CXHPnS-M.mjs → annotations-BYkjoj2d.mjs} +1 -1
  3. package/dist/{annotations-CXHPnS-M.mjs.map → annotations-BYkjoj2d.mjs.map} +1 -1
  4. package/dist/{apply-manager-llmSyK5K.mjs → apply-manager-B-VMXKiW.mjs} +3 -3
  5. package/dist/{apply-manager-llmSyK5K.mjs.map → apply-manager-B-VMXKiW.mjs.map} +1 -1
  6. package/dist/{astro-parser-dhFgc2wA.mjs → astro-parser-DsGdiUnu.mjs} +3 -3
  7. package/dist/{astro-parser-dhFgc2wA.mjs.map → astro-parser-DsGdiUnu.mjs.map} +1 -1
  8. package/dist/{availability-DYHNoP0_.mjs → availability-CO8Xfx3w.mjs} +1 -1
  9. package/dist/{availability-DYHNoP0_.mjs.map → availability-CO8Xfx3w.mjs.map} +1 -1
  10. package/dist/base-branch-Cw9DWvCC.mjs +121 -0
  11. package/dist/base-branch-Cw9DWvCC.mjs.map +1 -0
  12. package/dist/{branch-lifecycle-f4gI_eGo.mjs → branch-lifecycle-CcsSyfSw.mjs} +3 -68
  13. package/dist/branch-lifecycle-CcsSyfSw.mjs.map +1 -0
  14. package/dist/{branch-lifecycle-DGlgjnC7.d.mts → branch-lifecycle-CirYfTXu.d.mts} +1 -1
  15. package/dist/{branch-lifecycle-DGlgjnC7.d.mts.map → branch-lifecycle-CirYfTXu.d.mts.map} +1 -1
  16. package/dist/{compiler-CR9F0wZZ.mjs → compiler-CKelkozt.mjs} +2 -2
  17. package/dist/{compiler-CR9F0wZZ.mjs.map → compiler-CKelkozt.mjs.map} +1 -1
  18. package/dist/{compiler-sfc.cjs-Cpexs92i.mjs → compiler-sfc.cjs-BH5AEf2z.mjs} +2 -2
  19. package/dist/{compiler-sfc.cjs-Cpexs92i.mjs.map → compiler-sfc.cjs-BH5AEf2z.mjs.map} +1 -1
  20. package/dist/{content-manager-BqaMkMVk.d.mts → content-manager-2g0pkLFh.d.mts} +2 -2
  21. package/dist/{content-manager-BqaMkMVk.d.mts.map → content-manager-2g0pkLFh.d.mts.map} +1 -1
  22. package/dist/core/apply-manager.mjs +5 -4
  23. package/dist/core/config.d.mts +1 -1
  24. package/dist/core/content-manager.d.mts +1 -1
  25. package/dist/core/context.d.mts +1 -1
  26. package/dist/core/contracts/index.d.mts +2 -2
  27. package/dist/core/doctor.mjs +4 -3
  28. package/dist/core/graph-builder.mjs +2 -2
  29. package/dist/core/model-manager.d.mts +1 -1
  30. package/dist/core/ops/index.d.mts +3 -3
  31. package/dist/core/overlay-reader.d.mts +1 -1
  32. package/dist/core/scan-config.mjs +1 -1
  33. package/dist/core/scanner.mjs +3 -3
  34. package/dist/core/validator/index.d.mts +1 -1
  35. package/dist/{detect-DUYFv3-O.mjs → detect-B1QHYA0u.mjs} +1 -1
  36. package/dist/{detect-DUYFv3-O.mjs.map → detect-B1QHYA0u.mjs.map} +1 -1
  37. package/dist/{doctor-fIG4TtsO.mjs → doctor-Du4MVG5w.mjs} +3 -3
  38. package/dist/{doctor-fIG4TtsO.mjs.map → doctor-Du4MVG5w.mjs.map} +1 -1
  39. package/dist/{errors-DLkAWnzQ.mjs → errors-B0NcpPIp.mjs} +1 -1
  40. package/dist/{errors-DLkAWnzQ.mjs.map → errors-B0NcpPIp.mjs.map} +1 -1
  41. package/dist/{errors-BmLE-JSC.mjs → errors-De8Noahk.mjs} +2 -2
  42. package/dist/{errors-BmLE-JSC.mjs.map → errors-De8Noahk.mjs.map} +1 -1
  43. package/dist/git/base-branch.d.mts +61 -0
  44. package/dist/git/base-branch.d.mts.map +1 -0
  45. package/dist/git/base-branch.mjs +2 -0
  46. package/dist/git/branch-lifecycle.d.mts +1 -1
  47. package/dist/git/branch-lifecycle.mjs +2 -1
  48. package/dist/git/errors.mjs +1 -1
  49. package/dist/git/reconcile.d.mts +1 -1
  50. package/dist/git/reconcile.mjs +5 -4
  51. package/dist/git/transaction.d.mts +1 -1
  52. package/dist/git/transaction.mjs +4 -3
  53. package/dist/{graph-builder-CNswWNtb.mjs → graph-builder-BPhi-1KH.mjs} +2 -2
  54. package/dist/{graph-builder-CNswWNtb.mjs.map → graph-builder-BPhi-1KH.mjs.map} +1 -1
  55. package/dist/{id-CjGMPP1k.mjs → id-jrHWdtqn.mjs} +1 -1
  56. package/dist/{id-CjGMPP1k.mjs.map → id-jrHWdtqn.mjs.map} +1 -1
  57. package/dist/{index-DKZNUqpP.d.mts → index-BHpAYYFF.d.mts} +2 -2
  58. package/dist/{index-DKZNUqpP.d.mts.map → index-BHpAYYFF.d.mts.map} +1 -1
  59. package/dist/index-v0N9d4GT.d.mts +2 -0
  60. package/dist/index.mjs +16 -15
  61. package/dist/index.mjs.map +1 -1
  62. package/dist/{local-DkDTrGt_.mjs → local-BXTVAAr-.mjs} +314 -16
  63. package/dist/local-BXTVAAr-.mjs.map +1 -0
  64. package/dist/{node-CpLm3qr6.mjs → node-hrF4BrH2.mjs} +1 -1
  65. package/dist/{node-CpLm3qr6.mjs.map → node-hrF4BrH2.mjs.map} +1 -1
  66. package/dist/{paths-C6_C1gLp.mjs → paths-Dz5v-z00.mjs} +1 -1
  67. package/dist/{paths-C6_C1gLp.mjs.map → paths-Dz5v-z00.mjs.map} +1 -1
  68. package/dist/providers/github/index.d.mts +1 -1
  69. package/dist/providers/github/index.mjs +2 -2
  70. package/dist/providers/gitlab/index.d.mts +1 -1
  71. package/dist/providers/gitlab/index.mjs +2 -2
  72. package/dist/providers/local/index.d.mts +4 -1
  73. package/dist/providers/local/index.d.mts.map +1 -1
  74. package/dist/providers/local/index.mjs +5 -4
  75. package/dist/{reconcile-Cq60cldB.mjs → reconcile-BWbckvbv.mjs} +4 -3
  76. package/dist/{reconcile-Cq60cldB.mjs.map → reconcile-BWbckvbv.mjs.map} +1 -1
  77. package/dist/{scan-config-DkRKu_ON.mjs → scan-config-S5fkzNhL.mjs} +1 -1
  78. package/dist/{scan-config-DkRKu_ON.mjs.map → scan-config-S5fkzNhL.mjs.map} +1 -1
  79. package/dist/{scanner-BLo3ssiF.mjs → scanner-0sc_pARB.mjs} +6 -6
  80. package/dist/{scanner-BLo3ssiF.mjs.map → scanner-0sc_pARB.mjs.map} +1 -1
  81. package/dist/server/http/index.d.mts +1 -1
  82. package/dist/server/http/index.mjs +16 -15
  83. package/dist/server/http/index.mjs.map +1 -1
  84. package/dist/{server-CUtmhlno.d.mts → server-24UHN9Ll.d.mts} +2 -2
  85. package/dist/server-24UHN9Ll.d.mts.map +1 -0
  86. package/dist/{server-BJkE8D5N.mjs → server-Cl3LMiU3.mjs} +71 -44
  87. package/dist/server-Cl3LMiU3.mjs.map +1 -0
  88. package/dist/server.d.mts +1 -1
  89. package/dist/server.mjs +16 -15
  90. package/dist/{source-map-generator-Clb2Eqse.mjs → source-map-generator-DVDSWUZ1.mjs} +1 -1
  91. package/dist/{source-map-generator-Clb2Eqse.mjs.map → source-map-generator-DVDSWUZ1.mjs.map} +1 -1
  92. package/dist/{svelte-parser-CBN7Jxb7.mjs → svelte-parser-CVUIcakb.mjs} +3 -3
  93. package/dist/{svelte-parser-CBN7Jxb7.mjs.map → svelte-parser-CVUIcakb.mjs.map} +1 -1
  94. package/dist/templates/index.mjs +1 -1
  95. package/dist/{templates-BZbohJc8.mjs → templates-Dn-iZyzp.mjs} +1 -1
  96. package/dist/{templates-BZbohJc8.mjs.map → templates-Dn-iZyzp.mjs.map} +1 -1
  97. package/dist/testing/memory-provider.d.mts +1 -1
  98. package/dist/tools/annotations.mjs +1 -1
  99. package/dist/tools/availability.mjs +1 -1
  100. package/dist/{transaction-BWJt1KQb.mjs → transaction-DfrvGIfk.mjs} +4 -3
  101. package/dist/{transaction-BWJt1KQb.mjs.map → transaction-DfrvGIfk.mjs.map} +1 -1
  102. package/dist/{tsx-parser-go5V9zoc.mjs → tsx-parser-DgFdpsea.mjs} +1 -1
  103. package/dist/{tsx-parser-go5V9zoc.mjs.map → tsx-parser-DgFdpsea.mjs.map} +1 -1
  104. package/dist/tsx-parser-v8C-7cZv.mjs +2 -0
  105. package/dist/util/detect.mjs +1 -1
  106. package/dist/util/id.mjs +1 -1
  107. package/dist/{vue-parser-5Mp8w-mb.mjs → vue-parser-DvliI6UE.mjs} +3 -3
  108. package/dist/{vue-parser-5Mp8w-mb.mjs.map → vue-parser-DvliI6UE.mjs.map} +1 -1
  109. package/package.json +8 -4
  110. package/dist/branch-lifecycle-f4gI_eGo.mjs.map +0 -1
  111. package/dist/index-A5iw3yDk.d.mts +0 -2
  112. package/dist/local-DkDTrGt_.mjs.map +0 -1
  113. package/dist/server-BJkE8D5N.mjs.map +0 -1
  114. package/dist/server-CUtmhlno.d.mts.map +0 -1
  115. package/dist/tsx-parser-DOQpJVgK.mjs +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["createMcpServer"],"sources":["../../../src/server/http/server.ts"],"sourcesContent":["import http from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type { AddressInfo } from 'node:net'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { createServer as createMcpServer, type ToolProvider } from '../../server.js'\n\nexport interface HttpMcpServerOptions {\n /** Project root the MCP server operates against (LocalProvider path). */\n projectRoot: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\nexport interface HttpMcpServerProviderOptions {\n /** Pre-built content provider (GitHubProvider, a mock, etc.). */\n provider: ToolProvider\n /** Local project root when the provider is a LocalProvider; optional otherwise. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\n/**\n * Multi-tenant entry point — one resolver, many providers.\n *\n * Every new MCP session resolves its own `RepoProvider` from the incoming\n * HTTP request (typically via a workspace / project identifier in a\n * header or path segment). The resolver is invoked exactly once per\n * session; follow-up requests carrying the `Mcp-Session-Id` header are\n * routed to the existing `McpServer` + transport pair.\n *\n * Use this for Studio's hosted MCP Cloud or any agent that drives\n * multiple Contentrain projects through a single HTTP endpoint.\n */\nexport interface HttpMcpServerResolverOptions {\n /**\n * Per-session provider resolver. Invoked on the first request of a\n * new session (typically the MCP `initialize` call). Whatever\n * provider the resolver returns is bound to that session for its\n * lifetime.\n */\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n /** Optional fallback project root injected into each resolved session. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n /**\n * Idle-session TTL in ms. Sessions that haven't received a request\n * within this window are closed and their providers discarded.\n * Defaults to 15 minutes.\n */\n sessionTtlMs?: number\n /**\n * Optional tenant binding. When set, the fingerprint computed on the\n * session-creating request is stored with the session, and every\n * follow-up request carrying that `Mcp-Session-Id` must produce the\n * same fingerprint (e.g. derive it from the tenant headers the proxy\n * injects). A mismatch answers 404 — per the Streamable HTTP spec the\n * client then re-initializes and gets a session bound to its own\n * provider. This closes session-id replay across tenants.\n */\n sessionFingerprint?: (req: http.IncomingMessage) => string | undefined\n}\n\nexport interface HttpMcpServerHandle {\n server: http.Server\n /**\n * The single shared `McpServer` when the server was started with\n * `{ projectRoot }` or `{ provider }`. **Not set** in multi-tenant\n * resolver mode — in that mode each session owns its own server and\n * you should address them through the HTTP endpoint, not directly.\n */\n mcp: McpServer | null\n url: string\n close: () => Promise<void>\n}\n\nconst MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * Start an HTTP transport for the MCP server on top of the local project.\n *\n * Stateless, single-endpoint `POST /mcp`. The underlying `McpServer` is\n * the same one the stdio entry point uses, so every tool registered for\n * stdio is available over HTTP with no duplication. Only the transport\n * swaps out.\n *\n * Auth is a simple opaque Bearer token. When `authToken` is supplied,\n * any request without the exact `Authorization: Bearer <token>` header\n * gets a 401 before the MCP transport sees it. Non-local bind addresses\n * should always set this — the `contentrain serve --mcpHttp` CLI hard-\n * errors when it detects a non-localhost bind without a token.\n */\nexport async function startHttpMcpServer(\n opts: HttpMcpServerOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const mcp = createMcpServer(opts.projectRoot)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\n/**\n * Variant accepting a pre-built provider or a per-request resolver.\n *\n * **Single-provider mode** (`{ provider }`):\n * All sessions share one `McpServer` backed by the given provider.\n * Good for CI runners, single-tenant deployments, or any case where\n * the backing repository is fixed at boot time.\n *\n * **Multi-tenant mode** (`{ resolveProvider }`):\n * Each session resolves its own provider from the incoming request.\n * The resolver is invoked once per session; subsequent requests with\n * the `Mcp-Session-Id` header reuse the same server + transport pair.\n * Idle sessions are closed after `sessionTtlMs` (default 15m).\n */\nexport async function startHttpMcpServerWith(\n opts: (HttpMcpServerProviderOptions | HttpMcpServerResolverOptions) & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n if ('resolveProvider' in opts) {\n return startMultiTenantHttpMcpServer(opts)\n }\n const mcp = createMcpServer({ provider: opts.provider, projectRoot: opts.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\ninterface MultiTenantSession {\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n lastUsed: number\n /** Tenant fingerprint captured on the session-creating request (when `sessionFingerprint` is configured). */\n fingerprint: string | undefined\n}\n\nasync function startMultiTenantHttpMcpServer(\n opts: HttpMcpServerResolverOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const sessions = new Map<string, MultiTenantSession>()\n const ttl = opts.sessionTtlMs ?? 15 * 60 * 1000\n const mountPath = opts.path ?? '/mcp'\n const host = opts.host ?? '127.0.0.1'\n\n async function disposeSession(sessionId: string): Promise<void> {\n const session = sessions.get(sessionId)\n if (!session) return\n sessions.delete(sessionId)\n await session.transport.close().catch(() => { /* best-effort */ })\n await session.mcp.close().catch(() => { /* best-effort */ })\n }\n\n const reaper = setInterval(() => {\n const now = Date.now()\n for (const [id, session] of sessions) {\n if (now - session.lastUsed > ttl) void disposeSession(id)\n }\n }, Math.min(ttl, 60_000)).unref()\n\n const server = http.createServer((req, res) => {\n void handleMultiTenantRequest(req, res, {\n mountPath,\n authToken: opts.authToken,\n resolveProvider: opts.resolveProvider,\n projectRoot: opts.projectRoot,\n sessionFingerprint: opts.sessionFingerprint,\n sessions,\n })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(opts.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp: null,\n url,\n close: async () => {\n clearInterval(reaper)\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n const ids = [...sessions.keys()]\n await Promise.all(ids.map(id => disposeSession(id)))\n },\n }\n}\n\nasync function handleMultiTenantRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: {\n mountPath: string\n authToken?: string\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n projectRoot?: string\n sessionFingerprint?: (req: http.IncomingMessage) => string | undefined\n sessions: Map<string, MultiTenantSession>\n },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n const existingSessionId = pickSessionId(req.headers[MCP_SESSION_HEADER])\n if (existingSessionId && ctx.sessions.has(existingSessionId)) {\n const session = ctx.sessions.get(existingSessionId)!\n // Tenant binding: a session id minted for one tenant must not be\n // honored for another. Answer 404 (not 403) so a legitimate client\n // that landed on a foreign session re-initializes its own.\n if (ctx.sessionFingerprint && ctx.sessionFingerprint(req) !== session.fingerprint) {\n writeJson(res, 404, { error: 'Session not found' })\n return\n }\n session.lastUsed = Date.now()\n try {\n await session.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n return\n }\n\n try {\n const provider = await ctx.resolveProvider(req)\n const fingerprint = ctx.sessionFingerprint?.(req)\n const mcp = createMcpServer({ provider, projectRoot: ctx.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sessionId: string) => {\n ctx.sessions.set(sessionId, { mcp, transport, lastUsed: Date.now(), fingerprint })\n },\n })\n await mcp.connect(transport)\n await transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction pickSessionId(header: string | string[] | undefined): string | undefined {\n if (!header) return undefined\n return Array.isArray(header) ? header[0] : header\n}\n\nfunction applyRouteChecks(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { mountPath: string, authToken?: string },\n): boolean {\n if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {\n writeJson(res, 405, { error: 'Method Not Allowed' })\n return false\n }\n if (!req.url || !req.url.startsWith(ctx.mountPath)) {\n writeJson(res, 404, { error: 'Not Found' })\n return false\n }\n if (ctx.authToken) {\n const header = req.headers.authorization\n if (header !== `Bearer ${ctx.authToken}`) {\n writeJson(res, 401, { error: 'Unauthorized' })\n return false\n }\n }\n return true\n}\n\nasync function startHttpMcpServerInternal(input: {\n mode: 'single'\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n port: number\n host?: string\n authToken?: string\n path?: string\n}): Promise<HttpMcpServerHandle> {\n const { mcp, transport } = input\n const mountPath = input.path ?? '/mcp'\n const host = input.host ?? '127.0.0.1'\n\n const server = http.createServer((req, res) => {\n void handleRequest(req, res, { transport, mountPath, authToken: input.authToken })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(input.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp,\n url,\n close: async () => {\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n await transport.close().catch(() => { /* best-effort */ })\n await mcp.close().catch(() => { /* best-effort */ })\n },\n }\n}\n\nasync function handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { transport: StreamableHTTPServerTransport, mountPath: string, authToken?: string },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n try {\n await ctx.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction writeInternalError(res: http.ServerResponse, error: unknown): void {\n if (res.headersSent) return\n writeJson(res, 500, {\n error: 'Internal Server Error',\n message: error instanceof Error ? error.message : String(error),\n })\n}\n\nfunction writeJson(res: http.ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["createMcpServer"],"sources":["../../../src/server/http/server.ts"],"sourcesContent":["import http from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type { AddressInfo } from 'node:net'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { createServer as createMcpServer, type ToolProvider } from '../../server.js'\n\nexport interface HttpMcpServerOptions {\n /** Project root the MCP server operates against (LocalProvider path). */\n projectRoot: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\nexport interface HttpMcpServerProviderOptions {\n /** Pre-built content provider (GitHubProvider, a mock, etc.). */\n provider: ToolProvider\n /** Local project root when the provider is a LocalProvider; optional otherwise. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\n/**\n * Multi-tenant entry point — one resolver, many providers.\n *\n * Every new MCP session resolves its own `RepoProvider` from the incoming\n * HTTP request (typically via a workspace / project identifier in a\n * header or path segment). The resolver is invoked exactly once per\n * session; follow-up requests carrying the `Mcp-Session-Id` header are\n * routed to the existing `McpServer` + transport pair.\n *\n * Use this for Studio's hosted MCP Cloud or any agent that drives\n * multiple Contentrain projects through a single HTTP endpoint.\n */\nexport interface HttpMcpServerResolverOptions {\n /**\n * Per-session provider resolver. Invoked on the first request of a\n * new session (typically the MCP `initialize` call). Whatever\n * provider the resolver returns is bound to that session for its\n * lifetime.\n */\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n /** Optional fallback project root injected into each resolved session. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n /**\n * Idle-session TTL in ms. Sessions that haven't received a request\n * within this window are closed and their providers discarded.\n * Defaults to 15 minutes.\n */\n sessionTtlMs?: number\n /**\n * Optional tenant binding. When set, the fingerprint computed on the\n * session-creating request is stored with the session, and every\n * follow-up request carrying that `Mcp-Session-Id` must produce the\n * same fingerprint (e.g. derive it from the tenant headers the proxy\n * injects). A mismatch answers 404 — per the Streamable HTTP spec the\n * client then re-initializes and gets a session bound to its own\n * provider. This closes session-id replay across tenants.\n */\n sessionFingerprint?: (req: http.IncomingMessage) => string | undefined\n}\n\nexport interface HttpMcpServerHandle {\n server: http.Server\n /**\n * The single shared `McpServer` when the server was started with\n * `{ projectRoot }` or `{ provider }`. **Not set** in multi-tenant\n * resolver mode — in that mode each session owns its own server and\n * you should address them through the HTTP endpoint, not directly.\n */\n mcp: McpServer | null\n url: string\n close: () => Promise<void>\n}\n\nconst MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * Start an HTTP transport for the MCP server on top of the local project.\n *\n * Stateless, single-endpoint `POST /mcp`. The underlying `McpServer` is\n * the same one the stdio entry point uses, so every tool registered for\n * stdio is available over HTTP with no duplication. Only the transport\n * swaps out.\n *\n * Auth is a simple opaque Bearer token. When `authToken` is supplied,\n * any request without the exact `Authorization: Bearer <token>` header\n * gets a 401 before the MCP transport sees it. Non-local bind addresses\n * should always set this — the `contentrain serve --mcpHttp` CLI hard-\n * errors when it detects a non-localhost bind without a token.\n */\nexport async function startHttpMcpServer(\n opts: HttpMcpServerOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const mcp = createMcpServer(opts.projectRoot)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\n/**\n * Variant accepting a pre-built provider or a per-request resolver.\n *\n * **Single-provider mode** (`{ provider }`):\n * All sessions share one `McpServer` backed by the given provider.\n * Good for CI runners, single-tenant deployments, or any case where\n * the backing repository is fixed at boot time.\n *\n * **Multi-tenant mode** (`{ resolveProvider }`):\n * Each session resolves its own provider from the incoming request.\n * The resolver is invoked once per session; subsequent requests with\n * the `Mcp-Session-Id` header reuse the same server + transport pair.\n * Idle sessions are closed after `sessionTtlMs` (default 15m).\n */\nexport async function startHttpMcpServerWith(\n opts: (HttpMcpServerProviderOptions | HttpMcpServerResolverOptions) & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n if ('resolveProvider' in opts) {\n return startMultiTenantHttpMcpServer(opts)\n }\n const mcp = createMcpServer({ provider: opts.provider, projectRoot: opts.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\ninterface MultiTenantSession {\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n lastUsed: number\n /** Tenant fingerprint captured on the session-creating request (when `sessionFingerprint` is configured). */\n fingerprint: string | undefined\n}\n\nasync function startMultiTenantHttpMcpServer(\n opts: HttpMcpServerResolverOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const sessions = new Map<string, MultiTenantSession>()\n const ttl = opts.sessionTtlMs ?? 15 * 60 * 1000\n const mountPath = opts.path ?? '/mcp'\n const host = opts.host ?? '127.0.0.1'\n\n async function disposeSession(sessionId: string): Promise<void> {\n const session = sessions.get(sessionId)\n if (!session) return\n sessions.delete(sessionId)\n await session.transport.close().catch(() => { /* best-effort */ })\n await session.mcp.close().catch(() => { /* best-effort */ })\n }\n\n const reaper = setInterval(() => {\n const now = Date.now()\n for (const [id, session] of sessions) {\n if (now - session.lastUsed > ttl) void disposeSession(id)\n }\n }, Math.min(ttl, 60_000)).unref()\n\n const server = http.createServer((req, res) => {\n void handleMultiTenantRequest(req, res, {\n mountPath,\n authToken: opts.authToken,\n resolveProvider: opts.resolveProvider,\n projectRoot: opts.projectRoot,\n sessionFingerprint: opts.sessionFingerprint,\n sessions,\n })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(opts.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp: null,\n url,\n close: async () => {\n clearInterval(reaper)\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n const ids = [...sessions.keys()]\n await Promise.all(ids.map(id => disposeSession(id)))\n },\n }\n}\n\nasync function handleMultiTenantRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: {\n mountPath: string\n authToken?: string\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n projectRoot?: string\n sessionFingerprint?: (req: http.IncomingMessage) => string | undefined\n sessions: Map<string, MultiTenantSession>\n },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n const existingSessionId = pickSessionId(req.headers[MCP_SESSION_HEADER])\n if (existingSessionId && ctx.sessions.has(existingSessionId)) {\n const session = ctx.sessions.get(existingSessionId)!\n // Tenant binding: a session id minted for one tenant must not be\n // honored for another. Answer 404 (not 403) so a legitimate client\n // that landed on a foreign session re-initializes its own.\n if (ctx.sessionFingerprint && ctx.sessionFingerprint(req) !== session.fingerprint) {\n writeJson(res, 404, { error: 'Session not found' })\n return\n }\n session.lastUsed = Date.now()\n try {\n await session.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n return\n }\n\n try {\n const provider = await ctx.resolveProvider(req)\n const fingerprint = ctx.sessionFingerprint?.(req)\n const mcp = createMcpServer({ provider, projectRoot: ctx.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sessionId: string) => {\n ctx.sessions.set(sessionId, { mcp, transport, lastUsed: Date.now(), fingerprint })\n },\n })\n await mcp.connect(transport)\n await transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction pickSessionId(header: string | string[] | undefined): string | undefined {\n if (!header) return undefined\n return Array.isArray(header) ? header[0] : header\n}\n\nfunction applyRouteChecks(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { mountPath: string, authToken?: string },\n): boolean {\n if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {\n writeJson(res, 405, { error: 'Method Not Allowed' })\n return false\n }\n if (!req.url || !req.url.startsWith(ctx.mountPath)) {\n writeJson(res, 404, { error: 'Not Found' })\n return false\n }\n if (ctx.authToken) {\n const header = req.headers.authorization\n if (header !== `Bearer ${ctx.authToken}`) {\n writeJson(res, 401, { error: 'Unauthorized' })\n return false\n }\n }\n return true\n}\n\nasync function startHttpMcpServerInternal(input: {\n mode: 'single'\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n port: number\n host?: string\n authToken?: string\n path?: string\n}): Promise<HttpMcpServerHandle> {\n const { mcp, transport } = input\n const mountPath = input.path ?? '/mcp'\n const host = input.host ?? '127.0.0.1'\n\n const server = http.createServer((req, res) => {\n void handleRequest(req, res, { transport, mountPath, authToken: input.authToken })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(input.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp,\n url,\n close: async () => {\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n await transport.close().catch(() => { /* best-effort */ })\n await mcp.close().catch(() => { /* best-effort */ })\n },\n }\n}\n\nasync function handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { transport: StreamableHTTPServerTransport, mountPath: string, authToken?: string },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n try {\n await ctx.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction writeInternalError(res: http.ServerResponse, error: unknown): void {\n if (res.headersSent) return\n writeJson(res, 500, {\n error: 'Internal Server Error',\n message: error instanceof Error ? error.message : String(error),\n })\n}\n\nfunction writeJson(res: http.ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,MAAM,qBAAqB;;;;;;;;;;;;;;;AAgB3B,eAAsB,mBACpB,MAC8B;CAC9B,MAAM,MAAMA,aAAgB,KAAK,YAAY;CAC7C,MAAM,YAAY,IAAI,8BAA8B,EAClD,0BAA0B,YAAY,EACvC,CAAC;AACF,OAAM,IAAI,QAAQ,UAAU;AAE5B,QAAO,2BAA2B;EAChC,MAAM;EACN;EACA;EACA,MAAM,KAAK;EACX,MAAM,KAAK;EACX,WAAW,KAAK;EAChB,MAAM,KAAK;EACZ,CAAC;;;;;;;;;;;;;;;;AAiBJ,eAAsB,uBACpB,MAC8B;AAC9B,KAAI,qBAAqB,KACvB,QAAO,8BAA8B,KAAK;CAE5C,MAAM,MAAMA,aAAgB;EAAE,UAAU,KAAK;EAAU,aAAa,KAAK;EAAa,CAAC;CACvF,MAAM,YAAY,IAAI,8BAA8B,EAClD,0BAA0B,YAAY,EACvC,CAAC;AACF,OAAM,IAAI,QAAQ,UAAU;AAC5B,QAAO,2BAA2B;EAChC,MAAM;EACN;EACA;EACA,MAAM,KAAK;EACX,MAAM,KAAK;EACX,WAAW,KAAK;EAChB,MAAM,KAAK;EACZ,CAAC;;AAWJ,eAAe,8BACb,MAC8B;CAC9B,MAAM,2BAAW,IAAI,KAAiC;CACtD,MAAM,MAAM,KAAK,gBAAgB,MAAU;CAC3C,MAAM,YAAY,KAAK,QAAQ;CAC/B,MAAM,OAAO,KAAK,QAAQ;CAE1B,eAAe,eAAe,WAAkC;EAC9D,MAAM,UAAU,SAAS,IAAI,UAAU;AACvC,MAAI,CAAC,QAAS;AACd,WAAS,OAAO,UAAU;AAC1B,QAAM,QAAQ,UAAU,OAAO,CAAC,YAAY,GAAsB;AAClE,QAAM,QAAQ,IAAI,OAAO,CAAC,YAAY,GAAsB;;CAG9D,MAAM,SAAS,kBAAkB;EAC/B,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,MAAM,CAAC,IAAI,YAAY,SAC1B,KAAI,MAAM,QAAQ,WAAW,IAAU,gBAAe,GAAG;IAE1D,KAAK,IAAI,KAAK,IAAO,CAAC,CAAC,OAAO;CAEjC,MAAM,SAAS,KAAK,cAAc,KAAK,QAAQ;AACxC,2BAAyB,KAAK,KAAK;GACtC;GACA,WAAW,KAAK;GAChB,iBAAiB,KAAK;GACtB,aAAa,KAAK;GAClB,oBAAoB,KAAK;GACzB;GACD,CAAC;GACF;AAEF,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,KAAK,SAAS,OAAO;AAC5B,SAAO,OAAO,KAAK,MAAM,YAAY;AACnC,UAAO,IAAI,SAAS,OAAO;AAC3B,YAAS;IACT;GACF;AAKF,QAAO;EACL;EACA,KAAK;EACL,KALU,UAAU,KAAK,GADX,OAAO,SAAS,CACM,OAAO;EAM3C,OAAO,YAAY;AACjB,iBAAc,OAAO;AACrB,SAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,WAAO,OAAM,QAAQ,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACpD;GACF,MAAM,MAAM,CAAC,GAAG,SAAS,MAAM,CAAC;AAChC,SAAM,QAAQ,IAAI,IAAI,KAAI,OAAM,eAAe,GAAG,CAAC,CAAC;;EAEvD;;AAGH,eAAe,yBACb,KACA,KACA,KAQe;AACf,KAAI,CAAC,iBAAiB,KAAK,KAAK;EAAE,WAAW,IAAI;EAAW,WAAW,IAAI;EAAW,CAAC,CAAE;CAEzF,MAAM,oBAAoB,cAAc,IAAI,QAAQ,oBAAoB;AACxE,KAAI,qBAAqB,IAAI,SAAS,IAAI,kBAAkB,EAAE;EAC5D,MAAM,UAAU,IAAI,SAAS,IAAI,kBAAkB;AAInD,MAAI,IAAI,sBAAsB,IAAI,mBAAmB,IAAI,KAAK,QAAQ,aAAa;AACjF,aAAU,KAAK,KAAK,EAAE,OAAO,qBAAqB,CAAC;AACnD;;AAEF,UAAQ,WAAW,KAAK,KAAK;AAC7B,MAAI;AACF,SAAM,QAAQ,UAAU,cAAc,KAAK,IAAI;WACxC,OAAO;AACd,sBAAmB,KAAK,MAAM;;AAEhC;;AAGF,KAAI;EACF,MAAM,WAAW,MAAM,IAAI,gBAAgB,IAAI;EAC/C,MAAM,cAAc,IAAI,qBAAqB,IAAI;EACjD,MAAM,MAAMA,aAAgB;GAAE;GAAU,aAAa,IAAI;GAAa,CAAC;EACvE,MAAM,YAAY,IAAI,8BAA8B;GAClD,0BAA0B,YAAY;GACtC,uBAAuB,cAAsB;AAC3C,QAAI,SAAS,IAAI,WAAW;KAAE;KAAK;KAAW,UAAU,KAAK,KAAK;KAAE;KAAa,CAAC;;GAErF,CAAC;AACF,QAAM,IAAI,QAAQ,UAAU;AAC5B,QAAM,UAAU,cAAc,KAAK,IAAI;UAChC,OAAO;AACd,qBAAmB,KAAK,MAAM;;;AAIlC,SAAS,cAAc,QAA2D;AAChF,KAAI,CAAC,OAAQ,QAAO,KAAA;AACpB,QAAO,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK;;AAG7C,SAAS,iBACP,KACA,KACA,KACS;AACT,KAAI,IAAI,WAAW,UAAU,IAAI,WAAW,SAAS,IAAI,WAAW,UAAU;AAC5E,YAAU,KAAK,KAAK,EAAE,OAAO,sBAAsB,CAAC;AACpD,SAAO;;AAET,KAAI,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI,WAAW,IAAI,UAAU,EAAE;AAClD,YAAU,KAAK,KAAK,EAAE,OAAO,aAAa,CAAC;AAC3C,SAAO;;AAET,KAAI,IAAI;MACS,IAAI,QAAQ,kBACZ,UAAU,IAAI,aAAa;AACxC,aAAU,KAAK,KAAK,EAAE,OAAO,gBAAgB,CAAC;AAC9C,UAAO;;;AAGX,QAAO;;AAGT,eAAe,2BAA2B,OAQT;CAC/B,MAAM,EAAE,KAAK,cAAc;CAC3B,MAAM,YAAY,MAAM,QAAQ;CAChC,MAAM,OAAO,MAAM,QAAQ;CAE3B,MAAM,SAAS,KAAK,cAAc,KAAK,QAAQ;AACxC,gBAAc,KAAK,KAAK;GAAE;GAAW;GAAW,WAAW,MAAM;GAAW,CAAC;GAClF;AAEF,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,KAAK,SAAS,OAAO;AAC5B,SAAO,OAAO,MAAM,MAAM,YAAY;AACpC,UAAO,IAAI,SAAS,OAAO;AAC3B,YAAS;IACT;GACF;AAKF,QAAO;EACL;EACA;EACA,KALU,UAAU,KAAK,GADX,OAAO,SAAS,CACM,OAAO;EAM3C,OAAO,YAAY;AACjB,SAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,WAAO,OAAM,QAAQ,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACpD;AACF,SAAM,UAAU,OAAO,CAAC,YAAY,GAAsB;AAC1D,SAAM,IAAI,OAAO,CAAC,YAAY,GAAsB;;EAEvD;;AAGH,eAAe,cACb,KACA,KACA,KACe;AACf,KAAI,CAAC,iBAAiB,KAAK,KAAK;EAAE,WAAW,IAAI;EAAW,WAAW,IAAI;EAAW,CAAC,CAAE;AAEzF,KAAI;AACF,QAAM,IAAI,UAAU,cAAc,KAAK,IAAI;UACpC,OAAO;AACd,qBAAmB,KAAK,MAAM;;;AAIlC,SAAS,mBAAmB,KAA0B,OAAsB;AAC1E,KAAI,IAAI,YAAa;AACrB,WAAU,KAAK,KAAK;EAClB,OAAO;EACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;EAChE,CAAC;;AAGJ,SAAS,UAAU,KAA0B,QAAgB,MAAqB;AAChF,KAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AAC7D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC"}
@@ -1,4 +1,4 @@
1
- import { g as RepoProvider } from "./index-A5iw3yDk.mjs";
1
+ import { _ as RepoProvider } from "./index-v0N9d4GT.mjs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
 
4
4
  //#region src/server.d.ts
@@ -62,4 +62,4 @@ declare function createServer(projectRoot: string): McpServer;
62
62
  declare function createServer(opts: CreateServerOptions): McpServer;
63
63
  //#endregion
64
64
  export { createServer as i, DEFAULT_INSTRUCTIONS as n, ToolProvider as r, CreateServerOptions as t };
65
- //# sourceMappingURL=server-CUtmhlno.d.mts.map
65
+ //# sourceMappingURL=server-24UHN9Ll.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-24UHN9Ll.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;AAaA;;;;;KAAY,YAAA,GAAe,YAAA;;;;;AA6B3B;cAVa,oBAAA;AAAA,UAUI,mBAAA;EAOQ;;;;;;EAAvB,QAAA,GAAW,YAAA;EAuEG;;;;;AAChB;EAjEE,WAAA;;;;;;EAMA,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBA0Dc,YAAA,CAAa,WAAA,WAAsB,SAAA;AAAA,iBACnC,YAAA,CAAa,IAAA,EAAM,mBAAA,GAAsB,SAAA"}
@@ -7,25 +7,59 @@ import { t as OverlayReader } from "./overlay-reader-Bx1YxGXq.mjs";
7
7
  import { n as readVocabulary, t as readConfig } from "./config-BLwZtwlI.mjs";
8
8
  import { t as validateProject } from "./validator-C08p1OpM.mjs";
9
9
  import { r as createGit } from "./identity-CxAB9Qb8.mjs";
10
- import { a as contentBranchRelation, c as listRemoteCrBranches, d as resolveBaseBranch, i as cleanupMergedBranches, n as checkBranchHealth, o as deleteRemoteBranch, u as pruneMergedRemoteBranches } from "./branch-lifecycle-f4gI_eGo.mjs";
11
- import { i as ensureContentBranch, n as buildBranchName, o as mergeBranch, r as createTransaction } from "./transaction-BWJt1KQb.mjs";
12
- import { n as isMerged, t as LocalProvider } from "./local-DkDTrGt_.mjs";
13
- import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-DYHNoP0_.mjs";
14
- import { t as detectStack } from "./detect-DUYFv3-O.mjs";
15
- import { t as TOOL_ANNOTATIONS } from "./annotations-CXHPnS-M.mjs";
16
- import { n as listTemplates, t as getTemplate } from "./templates-BZbohJc8.mjs";
17
- import { t as normalizeOperationError } from "./errors-BmLE-JSC.mjs";
18
- import { t as reconcileBranches } from "./reconcile-Cq60cldB.mjs";
19
- import { t as buildGraph } from "./graph-builder-CNswWNtb.mjs";
20
- import { n as scanSummary, t as scanCandidates } from "./scanner-BLo3ssiF.mjs";
21
- import { n as applyExtract, r as applyReuse } from "./apply-manager-llmSyK5K.mjs";
22
- import { t as runDoctor } from "./doctor-fIG4TtsO.mjs";
10
+ import { r as resolveInitDefaultBranch, t as resolveBaseBranch } from "./base-branch-Cw9DWvCC.mjs";
11
+ import { a as contentBranchRelation, c as listRemoteCrBranches, i as cleanupMergedBranches, n as checkBranchHealth, o as deleteRemoteBranch, u as pruneMergedRemoteBranches } from "./branch-lifecycle-CcsSyfSw.mjs";
12
+ import { i as ensureContentBranch, n as buildBranchName, o as mergeBranch, r as createTransaction } from "./transaction-DfrvGIfk.mjs";
13
+ import { n as withReadScope, r as isMerged, t as LocalProvider } from "./local-BXTVAAr-.mjs";
14
+ import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-CO8Xfx3w.mjs";
15
+ import { t as detectStack } from "./detect-B1QHYA0u.mjs";
16
+ import { t as TOOL_ANNOTATIONS } from "./annotations-BYkjoj2d.mjs";
17
+ import { n as listTemplates, t as getTemplate } from "./templates-Dn-iZyzp.mjs";
18
+ import { t as normalizeOperationError } from "./errors-De8Noahk.mjs";
19
+ import { t as reconcileBranches } from "./reconcile-BWbckvbv.mjs";
20
+ import { t as buildGraph } from "./graph-builder-BPhi-1KH.mjs";
21
+ import { n as scanSummary, t as scanCandidates } from "./scanner-0sc_pARB.mjs";
22
+ import { n as applyExtract, r as applyReuse } from "./apply-manager-B-VMXKiW.mjs";
23
+ import { t as runDoctor } from "./doctor-Du4MVG5w.mjs";
23
24
  import { join } from "node:path";
24
25
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25
26
  import { appendFile, readFile, writeFile } from "node:fs/promises";
26
27
  import { CONTENTRAIN_BRANCH, MODEL_EXTENSION_KEYS, validateModelLocales } from "@contentrain/types";
27
28
  import { z } from "zod";
28
29
  import "simple-git";
30
+ //#region src/tools/guards.ts
31
+ /**
32
+ * Emit a uniform "capability not available" response for tools that
33
+ * require local filesystem access but are being driven by a remote
34
+ * provider (HTTP + GitHubProvider at the moment). The agent can use
35
+ * `capability_required` to decide whether to retry against a different
36
+ * transport or surface the limitation to the user.
37
+ */
38
+ function capabilityError(tool, capability) {
39
+ return {
40
+ content: [{
41
+ type: "text",
42
+ text: JSON.stringify({
43
+ error: `${tool} requires local filesystem access.`,
44
+ capability_required: capability,
45
+ hint: "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport."
46
+ })
47
+ }],
48
+ isError: true
49
+ };
50
+ }
51
+ /**
52
+ * The content source to report on a read tool's response, or null when the
53
+ * content came from where it always has (#229). Non-null only for a local
54
+ * provider on a feature branch, which reads `.contentrain/` from the
55
+ * `contentrain` ref: tools then read through the provider, not the
56
+ * working-tree fast paths, and say so as `content_source`.
57
+ */
58
+ async function refSource(provider) {
59
+ const source = await provider.contentSource?.();
60
+ return source?.source === "ref" ? source : null;
61
+ }
62
+ //#endregion
29
63
  //#region src/tools/context.ts
30
64
  function registerContextTools(server, provider, projectRoot) {
31
65
  server.tool("contentrain_status", "Get full project status (read-only). Returns config, models, context. Do NOT manually edit .contentrain/ based on this output.", {}, TOOL_ANNOTATIONS["contentrain_status"], async () => {
@@ -43,7 +77,8 @@ function registerContextTools(server, provider, projectRoot) {
43
77
  }
44
78
  const config = await readConfig(provider);
45
79
  const models = await listModels(provider);
46
- const context = projectRoot ? await readContext(projectRoot) : await readContext(provider);
80
+ const fromRef = await refSource(provider);
81
+ const context = projectRoot && !fromRef ? await readContext(projectRoot) : await readContext(provider);
47
82
  const vocabulary = await readVocabulary(provider);
48
83
  let stats = context?.stats ?? null;
49
84
  if (projectRoot) {
@@ -75,7 +110,8 @@ function registerContextTools(server, provider, projectRoot) {
75
110
  vocabulary: vocabulary && Object.keys(vocabulary.terms).length > 0 ? {
76
111
  size: Object.keys(vocabulary.terms).length,
77
112
  terms: vocabulary.terms
78
- } : { size: 0 }
113
+ } : { size: 0 },
114
+ ...fromRef ? { content_source: fromRef } : {}
79
115
  };
80
116
  if (projectRoot) {
81
117
  if (await pathExists(join(projectRoot, ".git"))) {
@@ -95,7 +131,7 @@ function registerContextTools(server, provider, projectRoot) {
95
131
  const git = createGit(projectRoot);
96
132
  const branches = await git.branchLocal();
97
133
  if (branches.all.includes(CONTENTRAIN_BRANCH)) {
98
- const baseBranch = await resolveBaseBranch(git, config, { currentBranch: branches.current });
134
+ const baseBranch = await resolveBaseBranch(git, await readConfig(projectRoot), { currentBranch: branches.current });
99
135
  const relation = await contentBranchRelation(git, baseBranch);
100
136
  result["content_branch"] = {
101
137
  base: baseBranch,
@@ -153,6 +189,8 @@ function registerContextTools(server, provider, projectRoot) {
153
189
  locales: stats.locales
154
190
  }
155
191
  };
192
+ const fromRef = await refSource(provider);
193
+ if (fromRef) result["content_source"] = fromRef;
156
194
  if (include_sample) {
157
195
  const sample = await getSample(provider, modelDef, effectiveLocale);
158
196
  if (sample) result["sample"] = sample;
@@ -428,28 +466,6 @@ function generateImportSnippet(model, stack, locale) {
428
466
  return snippets;
429
467
  }
430
468
  //#endregion
431
- //#region src/tools/guards.ts
432
- /**
433
- * Emit a uniform "capability not available" response for tools that
434
- * require local filesystem access but are being driven by a remote
435
- * provider (HTTP + GitHubProvider at the moment). The agent can use
436
- * `capability_required` to decide whether to retry against a different
437
- * transport or surface the limitation to the user.
438
- */
439
- function capabilityError(tool, capability) {
440
- return {
441
- content: [{
442
- type: "text",
443
- text: JSON.stringify({
444
- error: `${tool} requires local filesystem access.`,
445
- capability_required: capability,
446
- hint: "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport."
447
- })
448
- }],
449
- isError: true
450
- };
451
- }
452
- //#endregion
453
469
  //#region src/tools/commit-plan.ts
454
470
  /**
455
471
  * Context payload describing the operation, threaded into the local
@@ -593,6 +609,7 @@ function registerSetupTools(server, _provider, projectRoot) {
593
609
  }],
594
610
  isError: true
595
611
  };
612
+ const defaultBranch = await resolveInitDefaultBranch(git);
596
613
  const branch = buildBranchName("new", "init");
597
614
  const tx = await createTransaction(projectRoot, branch);
598
615
  try {
@@ -614,7 +631,8 @@ function registerSetupTools(server, _provider, projectRoot) {
614
631
  default: supportedLocales[0] ?? "en",
615
632
  supported: supportedLocales
616
633
  },
617
- domains: suggestedDomains
634
+ domains: suggestedDomains,
635
+ ...defaultBranch ? { repository: { default_branch: defaultBranch } } : {}
618
636
  };
619
637
  await writeJson(join(wtCrDir, "config.json"), config);
620
638
  await writeJson(join(wtCrDir, "vocabulary.json"), {
@@ -654,6 +672,7 @@ function registerSetupTools(server, _provider, projectRoot) {
654
672
  config_created: ".contentrain/config.json",
655
673
  detected_stack: detectedStack,
656
674
  detected_locales: supportedLocales,
675
+ default_branch: defaultBranch,
657
676
  suggested_domains: suggestedDomains,
658
677
  files_created: [
659
678
  ".contentrain/config.json",
@@ -1423,10 +1442,14 @@ function registerContentTools(server, provider, projectRoot) {
1423
1442
  limit: input.limit,
1424
1443
  offset: input.offset
1425
1444
  };
1426
- const result = projectRoot ? await listContent(projectRoot, model, listOpts, config) : await listContent(provider, model, listOpts, config);
1445
+ const fromRef = await refSource(provider);
1446
+ const result = projectRoot && !fromRef ? await listContent(projectRoot, model, listOpts, config) : await listContent(provider, model, listOpts, config);
1427
1447
  return { content: [{
1428
1448
  type: "text",
1429
- text: JSON.stringify(result, null, 2)
1449
+ text: JSON.stringify(fromRef ? {
1450
+ ...result,
1451
+ content_source: fromRef
1452
+ } : result, null, 2)
1430
1453
  }] };
1431
1454
  } catch (error) {
1432
1455
  return {
@@ -1749,7 +1772,8 @@ function registerWorkflowTools(server, provider, projectRoot) {
1749
1772
  await tx.cleanup();
1750
1773
  }
1751
1774
  }
1752
- if (!result) result = projectRoot ? await validateProject(projectRoot, {
1775
+ const fromRef = await refSource(provider);
1776
+ if (!result) result = projectRoot && !fromRef ? await validateProject(projectRoot, {
1753
1777
  model: input.model,
1754
1778
  fix: false
1755
1779
  }) : await validateProject(provider, {
@@ -1766,6 +1790,7 @@ function registerWorkflowTools(server, provider, projectRoot) {
1766
1790
  status: "validated",
1767
1791
  message: result.valid ? "All validation checks passed." : `Validation found ${result.summary.errors} error(s) and ${result.summary.warnings} warning(s).`,
1768
1792
  ...result,
1793
+ ...fromRef ? { content_source: fromRef } : {},
1769
1794
  next_steps: nextSteps
1770
1795
  }, null, 2)
1771
1796
  }] };
@@ -3026,7 +3051,7 @@ function registerMediaTools(server, provider, _projectRoot) {
3026
3051
  }
3027
3052
  //#endregion
3028
3053
  //#region package.json
3029
- var version = "3.6.8";
3054
+ var version = "3.8.0";
3030
3055
  //#endregion
3031
3056
  //#region src/server.ts
3032
3057
  /**
@@ -3049,6 +3074,8 @@ var CapabilityFilteredMcpServer = class extends McpServer {
3049
3074
  }
3050
3075
  tool(...args) {
3051
3076
  if (this._skipTools.has(args[0])) return;
3077
+ const handler = args.at(-1);
3078
+ if (typeof handler === "function") args[args.length - 1] = (...callArgs) => withReadScope(() => handler(...callArgs));
3052
3079
  return McpServer.prototype.tool.apply(this, args);
3053
3080
  }
3054
3081
  };
@@ -3101,4 +3128,4 @@ function resolveServerContext(input) {
3101
3128
  //#endregion
3102
3129
  export { createServer as n, DEFAULT_INSTRUCTIONS as t };
3103
3130
 
3104
- //# sourceMappingURL=server-BJkE8D5N.mjs.map
3131
+ //# sourceMappingURL=server-Cl3LMiU3.mjs.map