@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.
- package/README.md +2 -0
- package/dist/{annotations-CXHPnS-M.mjs → annotations-BYkjoj2d.mjs} +1 -1
- package/dist/{annotations-CXHPnS-M.mjs.map → annotations-BYkjoj2d.mjs.map} +1 -1
- package/dist/{apply-manager-llmSyK5K.mjs → apply-manager-B-VMXKiW.mjs} +3 -3
- package/dist/{apply-manager-llmSyK5K.mjs.map → apply-manager-B-VMXKiW.mjs.map} +1 -1
- package/dist/{astro-parser-dhFgc2wA.mjs → astro-parser-DsGdiUnu.mjs} +3 -3
- package/dist/{astro-parser-dhFgc2wA.mjs.map → astro-parser-DsGdiUnu.mjs.map} +1 -1
- package/dist/{availability-DYHNoP0_.mjs → availability-CO8Xfx3w.mjs} +1 -1
- package/dist/{availability-DYHNoP0_.mjs.map → availability-CO8Xfx3w.mjs.map} +1 -1
- package/dist/base-branch-Cw9DWvCC.mjs +121 -0
- package/dist/base-branch-Cw9DWvCC.mjs.map +1 -0
- package/dist/{branch-lifecycle-f4gI_eGo.mjs → branch-lifecycle-CcsSyfSw.mjs} +3 -68
- package/dist/branch-lifecycle-CcsSyfSw.mjs.map +1 -0
- package/dist/{branch-lifecycle-DGlgjnC7.d.mts → branch-lifecycle-CirYfTXu.d.mts} +1 -1
- package/dist/{branch-lifecycle-DGlgjnC7.d.mts.map → branch-lifecycle-CirYfTXu.d.mts.map} +1 -1
- package/dist/{compiler-CR9F0wZZ.mjs → compiler-CKelkozt.mjs} +2 -2
- package/dist/{compiler-CR9F0wZZ.mjs.map → compiler-CKelkozt.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-Cpexs92i.mjs → compiler-sfc.cjs-BH5AEf2z.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-Cpexs92i.mjs.map → compiler-sfc.cjs-BH5AEf2z.mjs.map} +1 -1
- package/dist/{content-manager-BqaMkMVk.d.mts → content-manager-2g0pkLFh.d.mts} +2 -2
- package/dist/{content-manager-BqaMkMVk.d.mts.map → content-manager-2g0pkLFh.d.mts.map} +1 -1
- package/dist/core/apply-manager.mjs +5 -4
- package/dist/core/config.d.mts +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/context.d.mts +1 -1
- package/dist/core/contracts/index.d.mts +2 -2
- package/dist/core/doctor.mjs +4 -3
- package/dist/core/graph-builder.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +3 -3
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/scan-config.mjs +1 -1
- package/dist/core/scanner.mjs +3 -3
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/{detect-DUYFv3-O.mjs → detect-B1QHYA0u.mjs} +1 -1
- package/dist/{detect-DUYFv3-O.mjs.map → detect-B1QHYA0u.mjs.map} +1 -1
- package/dist/{doctor-fIG4TtsO.mjs → doctor-Du4MVG5w.mjs} +3 -3
- package/dist/{doctor-fIG4TtsO.mjs.map → doctor-Du4MVG5w.mjs.map} +1 -1
- package/dist/{errors-DLkAWnzQ.mjs → errors-B0NcpPIp.mjs} +1 -1
- package/dist/{errors-DLkAWnzQ.mjs.map → errors-B0NcpPIp.mjs.map} +1 -1
- package/dist/{errors-BmLE-JSC.mjs → errors-De8Noahk.mjs} +2 -2
- package/dist/{errors-BmLE-JSC.mjs.map → errors-De8Noahk.mjs.map} +1 -1
- package/dist/git/base-branch.d.mts +61 -0
- package/dist/git/base-branch.d.mts.map +1 -0
- package/dist/git/base-branch.mjs +2 -0
- package/dist/git/branch-lifecycle.d.mts +1 -1
- package/dist/git/branch-lifecycle.mjs +2 -1
- package/dist/git/errors.mjs +1 -1
- package/dist/git/reconcile.d.mts +1 -1
- package/dist/git/reconcile.mjs +5 -4
- package/dist/git/transaction.d.mts +1 -1
- package/dist/git/transaction.mjs +4 -3
- package/dist/{graph-builder-CNswWNtb.mjs → graph-builder-BPhi-1KH.mjs} +2 -2
- package/dist/{graph-builder-CNswWNtb.mjs.map → graph-builder-BPhi-1KH.mjs.map} +1 -1
- package/dist/{id-CjGMPP1k.mjs → id-jrHWdtqn.mjs} +1 -1
- package/dist/{id-CjGMPP1k.mjs.map → id-jrHWdtqn.mjs.map} +1 -1
- package/dist/{index-DKZNUqpP.d.mts → index-BHpAYYFF.d.mts} +2 -2
- package/dist/{index-DKZNUqpP.d.mts.map → index-BHpAYYFF.d.mts.map} +1 -1
- package/dist/index-v0N9d4GT.d.mts +2 -0
- package/dist/index.mjs +16 -15
- package/dist/index.mjs.map +1 -1
- package/dist/{local-DkDTrGt_.mjs → local-BXTVAAr-.mjs} +314 -16
- package/dist/local-BXTVAAr-.mjs.map +1 -0
- package/dist/{node-CpLm3qr6.mjs → node-hrF4BrH2.mjs} +1 -1
- package/dist/{node-CpLm3qr6.mjs.map → node-hrF4BrH2.mjs.map} +1 -1
- package/dist/{paths-C6_C1gLp.mjs → paths-Dz5v-z00.mjs} +1 -1
- package/dist/{paths-C6_C1gLp.mjs.map → paths-Dz5v-z00.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/github/index.mjs +2 -2
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/gitlab/index.mjs +2 -2
- package/dist/providers/local/index.d.mts +4 -1
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +5 -4
- package/dist/{reconcile-Cq60cldB.mjs → reconcile-BWbckvbv.mjs} +4 -3
- package/dist/{reconcile-Cq60cldB.mjs.map → reconcile-BWbckvbv.mjs.map} +1 -1
- package/dist/{scan-config-DkRKu_ON.mjs → scan-config-S5fkzNhL.mjs} +1 -1
- package/dist/{scan-config-DkRKu_ON.mjs.map → scan-config-S5fkzNhL.mjs.map} +1 -1
- package/dist/{scanner-BLo3ssiF.mjs → scanner-0sc_pARB.mjs} +6 -6
- package/dist/{scanner-BLo3ssiF.mjs.map → scanner-0sc_pARB.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +16 -15
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-CUtmhlno.d.mts → server-24UHN9Ll.d.mts} +2 -2
- package/dist/server-24UHN9Ll.d.mts.map +1 -0
- package/dist/{server-BJkE8D5N.mjs → server-Cl3LMiU3.mjs} +71 -44
- package/dist/server-Cl3LMiU3.mjs.map +1 -0
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +16 -15
- package/dist/{source-map-generator-Clb2Eqse.mjs → source-map-generator-DVDSWUZ1.mjs} +1 -1
- package/dist/{source-map-generator-Clb2Eqse.mjs.map → source-map-generator-DVDSWUZ1.mjs.map} +1 -1
- package/dist/{svelte-parser-CBN7Jxb7.mjs → svelte-parser-CVUIcakb.mjs} +3 -3
- package/dist/{svelte-parser-CBN7Jxb7.mjs.map → svelte-parser-CVUIcakb.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-BZbohJc8.mjs → templates-Dn-iZyzp.mjs} +1 -1
- package/dist/{templates-BZbohJc8.mjs.map → templates-Dn-iZyzp.mjs.map} +1 -1
- package/dist/testing/memory-provider.d.mts +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/tools/availability.mjs +1 -1
- package/dist/{transaction-BWJt1KQb.mjs → transaction-DfrvGIfk.mjs} +4 -3
- package/dist/{transaction-BWJt1KQb.mjs.map → transaction-DfrvGIfk.mjs.map} +1 -1
- package/dist/{tsx-parser-go5V9zoc.mjs → tsx-parser-DgFdpsea.mjs} +1 -1
- package/dist/{tsx-parser-go5V9zoc.mjs.map → tsx-parser-DgFdpsea.mjs.map} +1 -1
- package/dist/tsx-parser-v8C-7cZv.mjs +2 -0
- package/dist/util/detect.mjs +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/{vue-parser-5Mp8w-mb.mjs → vue-parser-DvliI6UE.mjs} +3 -3
- package/dist/{vue-parser-5Mp8w-mb.mjs.map → vue-parser-DvliI6UE.mjs.map} +1 -1
- package/package.json +8 -4
- package/dist/branch-lifecycle-f4gI_eGo.mjs.map +0 -1
- package/dist/index-A5iw3yDk.d.mts +0 -2
- package/dist/local-DkDTrGt_.mjs.map +0 -1
- package/dist/server-BJkE8D5N.mjs.map +0 -1
- package/dist/server-CUtmhlno.d.mts.map +0 -1
- 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":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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 {
|
|
11
|
-
import { i as
|
|
12
|
-
import { n as
|
|
13
|
-
import { n as
|
|
14
|
-
import { t as
|
|
15
|
-
import { t as
|
|
16
|
-
import {
|
|
17
|
-
import { t as
|
|
18
|
-
import { t as
|
|
19
|
-
import { t as
|
|
20
|
-
import {
|
|
21
|
-
import { n as
|
|
22
|
-
import {
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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-
|
|
3131
|
+
//# sourceMappingURL=server-Cl3LMiU3.mjs.map
|