@contentrain/mcp 1.8.1 → 1.10.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 +13 -2
- package/dist/{annotations-BFJbfSKe.mjs → annotations-D3tlsF38.mjs} +77 -20
- package/dist/annotations-D3tlsF38.mjs.map +1 -0
- package/dist/{apply-manager-BRmB27H5.mjs → apply-manager-BnPbPXFG.mjs} +1 -1
- package/dist/{apply-manager-BRmB27H5.mjs.map → apply-manager-BnPbPXFG.mjs.map} +1 -1
- package/dist/{astro-parser-B-nLl0zE.mjs → astro-parser-BVoGjA6M.mjs} +3 -3
- package/dist/{astro-parser-B-nLl0zE.mjs.map → astro-parser-BVoGjA6M.mjs.map} +1 -1
- package/dist/availability-sWWRz2W0.mjs +53 -0
- package/dist/availability-sWWRz2W0.mjs.map +1 -0
- package/dist/{branch-lifecycle-7dQoF22r.d.mts → branch-lifecycle-AwBJIheA.d.mts} +1 -1
- package/dist/{branch-lifecycle-7dQoF22r.d.mts.map → branch-lifecycle-AwBJIheA.d.mts.map} +1 -1
- package/dist/{compiler-k4yttlzM.mjs → compiler-BZXAN4Ir.mjs} +2 -2
- package/dist/{compiler-k4yttlzM.mjs.map → compiler-BZXAN4Ir.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-D0d_OicF.mjs → compiler-sfc.cjs-tFlmtjrJ.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-D0d_OicF.mjs.map → compiler-sfc.cjs-tFlmtjrJ.mjs.map} +1 -1
- package/dist/{content-manager-DqDLSNQ-.d.mts → content-manager-DcmNwvmq.d.mts} +2 -2
- package/dist/{content-manager-DqDLSNQ-.d.mts.map → content-manager-DcmNwvmq.d.mts.map} +1 -1
- package/dist/core/apply-manager.mjs +1 -1
- 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 +2 -2
- package/dist/core/graph-builder.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +2 -2
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/overlay-reader.mjs +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/core/validator/index.mjs +1 -1
- package/dist/{detect-eWS6lvoH.mjs → detect-wVSI9VuY.mjs} +1 -1
- package/dist/{detect-eWS6lvoH.mjs.map → detect-wVSI9VuY.mjs.map} +1 -1
- package/dist/{doctor-qiUKCy9b.mjs → doctor-BTToHt0y.mjs} +2 -2
- package/dist/{doctor-qiUKCy9b.mjs.map → doctor-BTToHt0y.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts +1 -1
- package/dist/git/transaction.d.mts +1 -1
- package/dist/{graph-builder-DXv2JEp-.mjs → graph-builder-CRUX_8mA.mjs} +2 -2
- package/dist/{graph-builder-DXv2JEp-.mjs.map → graph-builder-CRUX_8mA.mjs.map} +1 -1
- package/dist/index-DDX-qYNw.d.mts +2 -0
- package/dist/index.mjs +10 -10
- package/dist/{node-DN9hZeil.mjs → node-wC-z3NoZ.mjs} +1 -1
- package/dist/{node-DN9hZeil.mjs.map → node-wC-z3NoZ.mjs.map} +1 -1
- package/dist/{overlay-reader-CUs9jgqk.mjs → overlay-reader-BOb105gS.mjs} +1 -1
- package/dist/{overlay-reader-CUs9jgqk.mjs.map → overlay-reader-BOb105gS.mjs.map} +1 -1
- package/dist/{paths-C3xNyc7z.mjs → paths-BU6E-oDs.mjs} +1 -1
- package/dist/{paths-C3xNyc7z.mjs.map → paths-BU6E-oDs.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/github/index.mjs +1 -1
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/gitlab/index.mjs +1 -1
- package/dist/providers/local/index.d.mts +2 -2
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/{scan-config-jC6XmiqF.mjs → scan-config-BGUflS8t.mjs} +1 -1
- package/dist/{scan-config-jC6XmiqF.mjs.map → scan-config-BGUflS8t.mjs.map} +1 -1
- package/dist/{scanner-DKMwc03k.mjs → scanner-VOwrKLGC.mjs} +6 -6
- package/dist/{scanner-DKMwc03k.mjs.map → scanner-VOwrKLGC.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +11 -1
- package/dist/server/http/index.d.mts.map +1 -1
- package/dist/server/http/index.mjs +20 -12
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-CXZFrEyu.mjs → server-DtabGAY6.mjs} +177 -18
- package/dist/server-DtabGAY6.mjs.map +1 -0
- package/dist/{server-DZBcZWhE.d.mts → server-vA1tM7DA.d.mts} +25 -10
- package/dist/server-vA1tM7DA.d.mts.map +1 -0
- package/dist/server.d.mts +2 -2
- package/dist/server.mjs +11 -11
- package/dist/{source-map-generator-BWDva49I.mjs → source-map-generator-BIEiz3If.mjs} +1 -1
- package/dist/{source-map-generator-BWDva49I.mjs.map → source-map-generator-BIEiz3If.mjs.map} +1 -1
- package/dist/{svelte-parser-Dr9mmZuk.mjs → svelte-parser-Bll-1SK0.mjs} +3 -3
- package/dist/{svelte-parser-Dr9mmZuk.mjs.map → svelte-parser-Bll-1SK0.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-VrLVqfe5.mjs → templates-D6EllixQ.mjs} +1 -1
- package/dist/{templates-VrLVqfe5.mjs.map → templates-D6EllixQ.mjs.map} +1 -1
- package/dist/tools/annotations.d.mts +3 -0
- package/dist/tools/annotations.d.mts.map +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/tools/availability.d.mts +39 -0
- package/dist/tools/availability.d.mts.map +1 -0
- package/dist/tools/availability.mjs +2 -0
- package/dist/{tsx-parser-CQc6nr-o.mjs → tsx-parser-B_aI_C2r.mjs} +1 -1
- package/dist/{tsx-parser-CQc6nr-o.mjs.map → tsx-parser-B_aI_C2r.mjs.map} +1 -1
- package/dist/tsx-parser-ChduVwKJ.mjs +2 -0
- package/dist/util/detect.mjs +1 -1
- package/dist/{validator-Z3DGNWQ6.mjs → validator-8vaPxSba.mjs} +1 -1
- package/dist/{validator-Z3DGNWQ6.mjs.map → validator-8vaPxSba.mjs.map} +1 -1
- package/dist/{vue-parser-COgjArqv.mjs → vue-parser-COig4Y1a.mjs} +3 -3
- package/dist/{vue-parser-COgjArqv.mjs.map → vue-parser-COig4Y1a.mjs.map} +1 -1
- package/package.json +11 -5
- package/dist/annotations-BFJbfSKe.mjs.map +0 -1
- package/dist/index-D0wQJSgs.d.mts +0 -2
- package/dist/server-CXZFrEyu.mjs.map +0 -1
- package/dist/server-DZBcZWhE.d.mts.map +0 -1
- package/dist/tsx-parser-BOMZE_yQ.mjs +0 -2
|
@@ -10,16 +10,16 @@ import "../../branch-lifecycle-D7tXOE4f.mjs";
|
|
|
10
10
|
import "../../id-DV_T9Ic8.mjs";
|
|
11
11
|
import "../../transaction-qliKvBRL.mjs";
|
|
12
12
|
import "../../local-BZEUWBoC.mjs";
|
|
13
|
-
import "../../detect-
|
|
14
|
-
import "../../annotations-
|
|
15
|
-
import {
|
|
16
|
-
import "../../validator-
|
|
17
|
-
import "../../scan-config-
|
|
18
|
-
import "../../graph-builder-
|
|
19
|
-
import "../../scanner-
|
|
20
|
-
import "../../tsx-parser-
|
|
21
|
-
import "../../apply-manager-
|
|
22
|
-
import "../../doctor-
|
|
13
|
+
import "../../detect-wVSI9VuY.mjs";
|
|
14
|
+
import "../../annotations-D3tlsF38.mjs";
|
|
15
|
+
import { n as createServer } from "../../server-DtabGAY6.mjs";
|
|
16
|
+
import "../../validator-8vaPxSba.mjs";
|
|
17
|
+
import "../../scan-config-BGUflS8t.mjs";
|
|
18
|
+
import "../../graph-builder-CRUX_8mA.mjs";
|
|
19
|
+
import "../../scanner-VOwrKLGC.mjs";
|
|
20
|
+
import "../../tsx-parser-B_aI_C2r.mjs";
|
|
21
|
+
import "../../apply-manager-BnPbPXFG.mjs";
|
|
22
|
+
import "../../doctor-BTToHt0y.mjs";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import http from "node:http";
|
|
25
25
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -107,6 +107,7 @@ async function startMultiTenantHttpMcpServer(opts) {
|
|
|
107
107
|
authToken: opts.authToken,
|
|
108
108
|
resolveProvider: opts.resolveProvider,
|
|
109
109
|
projectRoot: opts.projectRoot,
|
|
110
|
+
sessionFingerprint: opts.sessionFingerprint,
|
|
110
111
|
sessions
|
|
111
112
|
});
|
|
112
113
|
});
|
|
@@ -139,6 +140,10 @@ async function handleMultiTenantRequest(req, res, ctx) {
|
|
|
139
140
|
const existingSessionId = pickSessionId(req.headers[MCP_SESSION_HEADER]);
|
|
140
141
|
if (existingSessionId && ctx.sessions.has(existingSessionId)) {
|
|
141
142
|
const session = ctx.sessions.get(existingSessionId);
|
|
143
|
+
if (ctx.sessionFingerprint && ctx.sessionFingerprint(req) !== session.fingerprint) {
|
|
144
|
+
writeJson(res, 404, { error: "Session not found" });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
142
147
|
session.lastUsed = Date.now();
|
|
143
148
|
try {
|
|
144
149
|
await session.transport.handleRequest(req, res);
|
|
@@ -148,8 +153,10 @@ async function handleMultiTenantRequest(req, res, ctx) {
|
|
|
148
153
|
return;
|
|
149
154
|
}
|
|
150
155
|
try {
|
|
156
|
+
const provider = await ctx.resolveProvider(req);
|
|
157
|
+
const fingerprint = ctx.sessionFingerprint?.(req);
|
|
151
158
|
const mcp = createServer({
|
|
152
|
-
provider
|
|
159
|
+
provider,
|
|
153
160
|
projectRoot: ctx.projectRoot
|
|
154
161
|
});
|
|
155
162
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -158,7 +165,8 @@ async function handleMultiTenantRequest(req, res, ctx) {
|
|
|
158
165
|
ctx.sessions.set(sessionId, {
|
|
159
166
|
mcp,
|
|
160
167
|
transport,
|
|
161
|
-
lastUsed: Date.now()
|
|
168
|
+
lastUsed: Date.now(),
|
|
169
|
+
fingerprint
|
|
162
170
|
});
|
|
163
171
|
}
|
|
164
172
|
});
|
|
@@ -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\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}\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 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 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 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 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() })\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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"}
|
|
@@ -8,15 +8,16 @@ import { n as readContext } from "./context-CYN__o3Q.mjs";
|
|
|
8
8
|
import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-D7tXOE4f.mjs";
|
|
9
9
|
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-qliKvBRL.mjs";
|
|
10
10
|
import { n as isMerged, t as LocalProvider } from "./local-BZEUWBoC.mjs";
|
|
11
|
-
import { t as
|
|
12
|
-
import { t as
|
|
13
|
-
import {
|
|
14
|
-
import { t as
|
|
15
|
-
import { t as
|
|
16
|
-
import { t as
|
|
17
|
-
import {
|
|
18
|
-
import { n as
|
|
19
|
-
import {
|
|
11
|
+
import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-sWWRz2W0.mjs";
|
|
12
|
+
import { t as detectStack } from "./detect-wVSI9VuY.mjs";
|
|
13
|
+
import { t as TOOL_ANNOTATIONS } from "./annotations-D3tlsF38.mjs";
|
|
14
|
+
import { n as listTemplates, t as getTemplate } from "./templates-D6EllixQ.mjs";
|
|
15
|
+
import { t as validateProject } from "./validator-8vaPxSba.mjs";
|
|
16
|
+
import { t as OverlayReader } from "./overlay-reader-BOb105gS.mjs";
|
|
17
|
+
import { t as buildGraph } from "./graph-builder-CRUX_8mA.mjs";
|
|
18
|
+
import { n as scanSummary, t as scanCandidates } from "./scanner-VOwrKLGC.mjs";
|
|
19
|
+
import { n as applyExtract, r as applyReuse } from "./apply-manager-BnPbPXFG.mjs";
|
|
20
|
+
import { t as runDoctor } from "./doctor-BTToHt0y.mjs";
|
|
20
21
|
import { join } from "node:path";
|
|
21
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
23
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
@@ -2367,16 +2368,169 @@ function registerDoctorTools(server, _provider, projectRoot) {
|
|
|
2367
2368
|
});
|
|
2368
2369
|
}
|
|
2369
2370
|
//#endregion
|
|
2371
|
+
//#region src/tools/media.ts
|
|
2372
|
+
/**
|
|
2373
|
+
* Media tools — a deterministic passthrough to the provider's optional
|
|
2374
|
+
* media facet (`RepoProvider.media`). Registration is capability-aware:
|
|
2375
|
+
* `createServer` skips all five tools when the provider has no media
|
|
2376
|
+
* stack, so the guard below is defense in depth only.
|
|
2377
|
+
*
|
|
2378
|
+
* The facet closes the discovery loop for external agents: list assets →
|
|
2379
|
+
* pick a `media/...` path → reference it via `contentrain_content_save`
|
|
2380
|
+
* (which normalizes it to an absolute delivery URL when `mediaBaseUrl`
|
|
2381
|
+
* is set). Ingest is URL-based — MCP has no binary channel — and the
|
|
2382
|
+
* provider implementation owns SSRF/MIME/size policy for the fetch.
|
|
2383
|
+
*/
|
|
2384
|
+
function mediaUnavailable(tool) {
|
|
2385
|
+
return {
|
|
2386
|
+
content: [{
|
|
2387
|
+
type: "text",
|
|
2388
|
+
text: JSON.stringify({
|
|
2389
|
+
error: `${tool} requires a provider with a media stack.`,
|
|
2390
|
+
capability_required: "media",
|
|
2391
|
+
hint: "Media tools are available when MCP is driven by a media-capable provider (e.g. Studio MCP Cloud). Local and plain git providers do not expose one."
|
|
2392
|
+
})
|
|
2393
|
+
}],
|
|
2394
|
+
isError: true
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
function mediaError(error, stage) {
|
|
2398
|
+
return {
|
|
2399
|
+
content: [{
|
|
2400
|
+
type: "text",
|
|
2401
|
+
text: JSON.stringify({ ...normalizeOperationError(error, stage) })
|
|
2402
|
+
}],
|
|
2403
|
+
isError: true
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
function ok(payload) {
|
|
2407
|
+
return { content: [{
|
|
2408
|
+
type: "text",
|
|
2409
|
+
text: JSON.stringify(payload)
|
|
2410
|
+
}] };
|
|
2411
|
+
}
|
|
2412
|
+
function registerMediaTools(server, provider, _projectRoot) {
|
|
2413
|
+
server.tool("contentrain_media_list", "List media assets from the provider media stack. Returns storage paths (media/...) usable in media/image/file fields via contentrain_content_save, plus delivery URLs when available. Supports search, tag filter, and cursor pagination.", {
|
|
2414
|
+
search: z.string().optional().describe("Substring match on filename/path/alt"),
|
|
2415
|
+
tag: z.string().optional().describe("Only assets carrying this tag"),
|
|
2416
|
+
limit: z.number().int().min(1).max(100).optional().describe("Page size. Default: provider-defined"),
|
|
2417
|
+
cursor: z.string().optional().describe("Opaque cursor from a previous response")
|
|
2418
|
+
}, TOOL_ANNOTATIONS["contentrain_media_list"], async (input) => {
|
|
2419
|
+
if (!provider.media) return mediaUnavailable("contentrain_media_list");
|
|
2420
|
+
try {
|
|
2421
|
+
const result = await provider.media.list(input);
|
|
2422
|
+
return ok({
|
|
2423
|
+
assets: result.assets,
|
|
2424
|
+
next_cursor: result.nextCursor,
|
|
2425
|
+
total: result.total
|
|
2426
|
+
});
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
return mediaError(error, "media_list");
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
server.tool("contentrain_media_get", "Get a single media asset by id, including its storage path, delivery URL, and metadata.", { id: z.string().describe("Asset id (from contentrain_media_list)") }, TOOL_ANNOTATIONS["contentrain_media_get"], async (input) => {
|
|
2432
|
+
if (!provider.media) return mediaUnavailable("contentrain_media_get");
|
|
2433
|
+
try {
|
|
2434
|
+
const asset = await provider.media.get(input.id);
|
|
2435
|
+
if (!asset) return {
|
|
2436
|
+
content: [{
|
|
2437
|
+
type: "text",
|
|
2438
|
+
text: JSON.stringify({
|
|
2439
|
+
error: `Media asset not found: ${input.id}`,
|
|
2440
|
+
hint: "Use contentrain_media_list to discover asset ids."
|
|
2441
|
+
})
|
|
2442
|
+
}],
|
|
2443
|
+
isError: true
|
|
2444
|
+
};
|
|
2445
|
+
return ok({ asset });
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
return mediaError(error, "media_get");
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
server.tool("contentrain_media_ingest", "Ingest a media asset from a source URL. The provider fetches the URL server-side under its own SSRF/MIME/size policy and stores the asset in the media stack. Returns the stored asset with its media/... path.", {
|
|
2451
|
+
url: z.string().url().describe("Source URL to fetch server-side"),
|
|
2452
|
+
filename: z.string().optional().describe("Target filename override"),
|
|
2453
|
+
alt: z.string().optional().describe("Alt text for the asset"),
|
|
2454
|
+
tags: z.array(z.string()).max(20).optional().describe("Tags to attach")
|
|
2455
|
+
}, TOOL_ANNOTATIONS["contentrain_media_ingest"], async (input) => {
|
|
2456
|
+
if (!provider.media) return mediaUnavailable("contentrain_media_ingest");
|
|
2457
|
+
try {
|
|
2458
|
+
return ok({
|
|
2459
|
+
status: "ingested",
|
|
2460
|
+
asset: await provider.media.ingest(input)
|
|
2461
|
+
});
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
return mediaError(error, "media_ingest");
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
server.tool("contentrain_media_update", "Update metadata (alt text, tags, filename) of an existing media asset. Does not touch the binary.", {
|
|
2467
|
+
id: z.string().describe("Asset id to update"),
|
|
2468
|
+
alt: z.string().optional().describe("New alt text"),
|
|
2469
|
+
tags: z.array(z.string()).max(20).optional().describe("Replacement tag list"),
|
|
2470
|
+
filename: z.string().optional().describe("New filename")
|
|
2471
|
+
}, TOOL_ANNOTATIONS["contentrain_media_update"], async (input) => {
|
|
2472
|
+
if (!provider.media) return mediaUnavailable("contentrain_media_update");
|
|
2473
|
+
try {
|
|
2474
|
+
const { id, ...patch } = input;
|
|
2475
|
+
return ok({
|
|
2476
|
+
status: "updated",
|
|
2477
|
+
asset: await provider.media.update(id, patch)
|
|
2478
|
+
});
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
return mediaError(error, "media_update");
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
server.tool("contentrain_media_delete", "Delete a media asset from the media stack. Content entries referencing its path are NOT rewritten — check references before deleting.", {
|
|
2484
|
+
id: z.string().describe("Asset id to delete"),
|
|
2485
|
+
confirm: z.literal(true).describe("Must be true to confirm deletion")
|
|
2486
|
+
}, TOOL_ANNOTATIONS["contentrain_media_delete"], async (input) => {
|
|
2487
|
+
if (!provider.media) return mediaUnavailable("contentrain_media_delete");
|
|
2488
|
+
try {
|
|
2489
|
+
await provider.media.delete(input.id);
|
|
2490
|
+
return ok({
|
|
2491
|
+
status: "deleted",
|
|
2492
|
+
id: input.id
|
|
2493
|
+
});
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
return mediaError(error, "media_delete");
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
//#endregion
|
|
2370
2500
|
//#region package.json
|
|
2371
|
-
var version = "1.
|
|
2501
|
+
var version = "1.10.0";
|
|
2372
2502
|
//#endregion
|
|
2373
2503
|
//#region src/server.ts
|
|
2504
|
+
/**
|
|
2505
|
+
* Default MCP `instructions` surfaced to clients at initialize time.
|
|
2506
|
+
* Deliberately kept under 512 characters — directory listings and client
|
|
2507
|
+
* UIs truncate longer strings. Override via `CreateServerOptions.instructions`.
|
|
2508
|
+
*/
|
|
2509
|
+
const DEFAULT_INSTRUCTIONS = "Contentrain is git-native content governance: models define structure; content is canonical JSON/Markdown on a dedicated branch. Call contentrain_describe_format before creating models or content. Preview writes with dry_run:true, review the plan, then re-run with dry_run:false. Start with contentrain_status for models, locales, and workflow; inspect a model with contentrain_describe before editing its entries. Tools are deterministic infrastructure — content decisions stay with the agent.";
|
|
2510
|
+
/**
|
|
2511
|
+
* `McpServer` variant that silently skips registration for tools named in
|
|
2512
|
+
* `skipTools`. Register functions call `server.tool(...)` unconditionally;
|
|
2513
|
+
* this subclass is what keeps capability-gated tools out of `tools/list`
|
|
2514
|
+
* when the provider (or missing projectRoot) could never satisfy them.
|
|
2515
|
+
*/
|
|
2516
|
+
var CapabilityFilteredMcpServer = class extends McpServer {
|
|
2517
|
+
_skipTools;
|
|
2518
|
+
constructor(serverInfo, options, skipTools) {
|
|
2519
|
+
super(serverInfo, options);
|
|
2520
|
+
this._skipTools = skipTools;
|
|
2521
|
+
}
|
|
2522
|
+
tool(...args) {
|
|
2523
|
+
if (this._skipTools.has(args[0])) return;
|
|
2524
|
+
return McpServer.prototype.tool.apply(this, args);
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2374
2527
|
function createServer(input) {
|
|
2375
|
-
const { provider, projectRoot } = resolveServerContext(input);
|
|
2376
|
-
const
|
|
2528
|
+
const { provider, projectRoot, instructions } = resolveServerContext(input);
|
|
2529
|
+
const skipTools = new Set(Object.keys(TOOL_REQUIREMENTS).filter((name) => !isToolAvailable(name, provider, projectRoot)));
|
|
2530
|
+
const server = new CapabilityFilteredMcpServer({
|
|
2377
2531
|
name: "contentrain-mcp",
|
|
2378
2532
|
version
|
|
2379
|
-
});
|
|
2533
|
+
}, { instructions: instructions || void 0 }, skipTools);
|
|
2380
2534
|
registerContextTools(server, provider, projectRoot);
|
|
2381
2535
|
registerSetupTools(server, provider, projectRoot);
|
|
2382
2536
|
registerModelTools(server, provider, projectRoot);
|
|
@@ -2385,28 +2539,33 @@ function createServer(input) {
|
|
|
2385
2539
|
registerNormalizeTools(server, provider, projectRoot);
|
|
2386
2540
|
registerBulkTools(server, provider, projectRoot);
|
|
2387
2541
|
registerDoctorTools(server, provider, projectRoot);
|
|
2542
|
+
registerMediaTools(server, provider, projectRoot);
|
|
2388
2543
|
return server;
|
|
2389
2544
|
}
|
|
2390
2545
|
function resolveServerContext(input) {
|
|
2391
2546
|
if (typeof input === "string") return {
|
|
2392
2547
|
provider: new LocalProvider(input),
|
|
2393
|
-
projectRoot: input
|
|
2548
|
+
projectRoot: input,
|
|
2549
|
+
instructions: DEFAULT_INSTRUCTIONS
|
|
2394
2550
|
};
|
|
2551
|
+
const instructions = input.instructions ?? "Contentrain is git-native content governance: models define structure; content is canonical JSON/Markdown on a dedicated branch. Call contentrain_describe_format before creating models or content. Preview writes with dry_run:true, review the plan, then re-run with dry_run:false. Start with contentrain_status for models, locales, and workflow; inspect a model with contentrain_describe before editing its entries. Tools are deterministic infrastructure — content decisions stay with the agent.";
|
|
2395
2552
|
if (input.provider) {
|
|
2396
2553
|
let projectRoot = input.projectRoot;
|
|
2397
2554
|
if (!projectRoot && input.provider instanceof LocalProvider) projectRoot = input.provider.projectRoot;
|
|
2398
2555
|
return {
|
|
2399
2556
|
provider: input.provider,
|
|
2400
|
-
projectRoot
|
|
2557
|
+
projectRoot,
|
|
2558
|
+
instructions
|
|
2401
2559
|
};
|
|
2402
2560
|
}
|
|
2403
2561
|
if (input.projectRoot) return {
|
|
2404
2562
|
provider: new LocalProvider(input.projectRoot),
|
|
2405
|
-
projectRoot: input.projectRoot
|
|
2563
|
+
projectRoot: input.projectRoot,
|
|
2564
|
+
instructions
|
|
2406
2565
|
};
|
|
2407
2566
|
throw new Error("createServer: either `provider` or `projectRoot` must be provided");
|
|
2408
2567
|
}
|
|
2409
2568
|
//#endregion
|
|
2410
|
-
export { createServer as t };
|
|
2569
|
+
export { createServer as n, DEFAULT_INSTRUCTIONS as t };
|
|
2411
2570
|
|
|
2412
|
-
//# sourceMappingURL=server-
|
|
2571
|
+
//# sourceMappingURL=server-DtabGAY6.mjs.map
|