@contentrain/mcp 3.11.10 → 3.11.12

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 CHANGED
@@ -170,7 +170,7 @@ const transport = new StdioServerTransport()
170
170
  await server.connect(transport)
171
171
  ```
172
172
 
173
- `createServer` also accepts an options object: `{ provider, projectRoot?, instructions? }`. `instructions` sets the MCP `instructions` string clients receive at `initialize` (defaults to a built-in `DEFAULT_INSTRUCTIONS`, kept under 512 characters; pass `''` to omit).
173
+ `createServer` also accepts an options object: `{ provider, projectRoot?, instructions? }`. `instructions` sets the MCP `instructions` string clients receive at `initialize` (defaults to `defaultInstructions` for the tools the server registers — with every tool available, `DEFAULT_INSTRUCTIONS` — kept under 512 characters; pass `''` to omit). The HTTP entries (`startHttpMcpServer`, `startHttpMcpServerWith`) take the same `instructions` option.
174
174
 
175
175
  ## Example MCP Flow
176
176
 
package/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ import "./transaction-DCDf0y0T.mjs";
16
16
  import "./local-CCVjLKy2.mjs";
17
17
  import "./detect-B1QHYA0u.mjs";
18
18
  import "./annotations-BYkjoj2d.mjs";
19
- import { n as createServer } from "./server-BI_lvugX.mjs";
19
+ import { n as createServer } from "./server-lRjGDFMw.mjs";
20
20
  import "./errors-Cp6_Hlws.mjs";
21
21
  import "./reconcile-B7w5gow1.mjs";
22
22
  import "./scan-config-S5fkzNhL.mjs";
@@ -1,4 +1,4 @@
1
- import { r as ToolProvider } from "../../server-CQKCQ8H_.mjs";
1
+ import { r as ToolProvider } from "../../server-rqy4Gqf0.mjs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import http from "node:http";
4
4
 
@@ -10,6 +10,8 @@ interface HttpMcpServerOptions {
10
10
  authToken?: string;
11
11
  /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */
12
12
  path?: string;
13
+ /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */
14
+ instructions?: string;
13
15
  }
14
16
  interface HttpMcpServerProviderOptions {
15
17
  /** Pre-built content provider (GitHubProvider, a mock, etc.). */
@@ -20,6 +22,8 @@ interface HttpMcpServerProviderOptions {
20
22
  authToken?: string;
21
23
  /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */
22
24
  path?: string;
25
+ /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */
26
+ instructions?: string;
23
27
  }
24
28
  /**
25
29
  * Multi-tenant entry point — one resolver, many providers.
@@ -47,6 +51,8 @@ interface HttpMcpServerResolverOptions {
47
51
  authToken?: string;
48
52
  /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */
49
53
  path?: string;
54
+ /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */
55
+ instructions?: string;
50
56
  /**
51
57
  * Idle-session TTL in ms. Sessions that haven't received a request
52
58
  * within this window are closed and their providers discarded.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/server/http/server.ts"],"mappings":";;;;;UAOiB,oBAAA;;EAEf,WAAA;EAFe;EAIf,SAAA;;EAEA,IAAA;AAAA;AAAA,UAGe,4BAAA;EAHf;EAKA,QAAA,EAAU,YAAA;EALN;EAOJ,WAAA;EAJ2C;EAM3C,SAAA;EAJsB;EAMtB,IAAA;AAAA;;;;;;AAeF;;;;;;;UAAiB,4BAAA;EA6BgC;;;;;;EAtB/C,eAAA,GAAkB,GAAA,EAAK,IAAA,CAAK,eAAA,KAAoB,YAAA,GAAe,OAAA,CAAQ,YAAA;EAAR;EAE/D,WAAA;EAAA;EAEA,SAAA;EAEA;EAAA,IAAA;EAgBA;;;;;EAVA,YAAA;EAae;;;;;;;;;EAHf,kBAAA,IAAsB,GAAA,EAAK,IAAA,CAAK,eAAA;AAAA;AAAA,UAGjB,mBAAA;EACf,MAAA,EAAQ,IAAA,CAAK,MAAA;EAQb;;;;;AAoBF;EArBE,GAAA,EAAK,SAAA;EACL,GAAA;EACA,KAAA,QAAa,OAAA;AAAA;;;;;;;;;;;;AAqDf;;;iBAlCsB,kBAAA,CACpB,IAAA,EAAM,oBAAA;EAAyB,IAAA;EAAc,IAAA;AAAA,IAC5C,OAAA,CAAQ,mBAAA;;;;;;;;;;;;;;;iBAgCW,sBAAA,CACpB,IAAA,GAAO,4BAAA,GAA+B,4BAAA;EAAkC,IAAA;EAAc,IAAA;AAAA,IACrF,OAAA,CAAQ,mBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/server/http/server.ts"],"mappings":";;;;;UAOiB,oBAAA;;EAEf,WAAA;EAFe;EAIf,SAAA;;EAEA,IAAA;EAJA;EAMA,YAAA;AAAA;AAAA,UAGe,4BAAA;EAHH;EAKZ,QAAA,EAAU,YAAA;EAFK;EAIf,WAAA;;EAEA,SAAA;EAJA;EAMA,IAAA;EAJA;EAMA,YAAA;AAAA;;;;AAeF;;;;;;;;;UAAiB,4BAAA;EAOf;;;;;;EAAA,eAAA,GAAkB,GAAA,EAAK,IAAA,CAAK,eAAA,KAAoB,YAAA,GAAe,OAAA,CAAQ,YAAA;EAEvE;EAAA,WAAA;EAIA;EAFA,SAAA;EAUA;EARA,IAAA;EAkB2B;EAhB3B,YAAA;EAgBsB;;;AAGxB;;EAbE,YAAA;EAcQ;;;;;;;;;EAJR,kBAAA,IAAsB,GAAA,EAAK,IAAA,CAAK,eAAA;AAAA;AAAA,UAGjB,mBAAA;EACf,MAAA,EAAQ,IAAA,CAAK,MAAA;EASO;;AAmBtB;;;;EArBE,GAAA,EAAK,SAAA;EACL,GAAA;EACA,KAAA,QAAa,OAAA;AAAA;;;;;;;;;AAqDf;;;;;;iBAlCsB,kBAAA,CACpB,IAAA,EAAM,oBAAA;EAAyB,IAAA;EAAc,IAAA;AAAA,IAC5C,OAAA,CAAQ,mBAAA;;;;;;;;;;;;;;;iBAgCW,sBAAA,CACpB,IAAA,GAAO,4BAAA,GAA+B,4BAAA;EAAkC,IAAA;EAAc,IAAA;AAAA,IACrF,OAAA,CAAQ,mBAAA"}
@@ -15,7 +15,7 @@ import "../../transaction-DCDf0y0T.mjs";
15
15
  import "../../local-CCVjLKy2.mjs";
16
16
  import "../../detect-B1QHYA0u.mjs";
17
17
  import "../../annotations-BYkjoj2d.mjs";
18
- import { n as createServer } from "../../server-BI_lvugX.mjs";
18
+ import { n as createServer } from "../../server-lRjGDFMw.mjs";
19
19
  import "../../errors-Cp6_Hlws.mjs";
20
20
  import "../../reconcile-B7w5gow1.mjs";
21
21
  import "../../scan-config-S5fkzNhL.mjs";
@@ -44,7 +44,10 @@ const MCP_SESSION_HEADER = "mcp-session-id";
44
44
  * errors when it detects a non-localhost bind without a token.
45
45
  */
46
46
  async function startHttpMcpServer(opts) {
47
- const mcp = createServer(opts.projectRoot);
47
+ const mcp = createServer({
48
+ projectRoot: opts.projectRoot,
49
+ ...opts.instructions === void 0 ? {} : { instructions: opts.instructions }
50
+ });
48
51
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
49
52
  await mcp.connect(transport);
50
53
  return startHttpMcpServerInternal({
@@ -75,7 +78,8 @@ async function startHttpMcpServerWith(opts) {
75
78
  if ("resolveProvider" in opts) return startMultiTenantHttpMcpServer(opts);
76
79
  const mcp = createServer({
77
80
  provider: opts.provider,
78
- projectRoot: opts.projectRoot
81
+ projectRoot: opts.projectRoot,
82
+ ...opts.instructions === void 0 ? {} : { instructions: opts.instructions }
79
83
  });
80
84
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
81
85
  await mcp.connect(transport);
@@ -111,6 +115,7 @@ async function startMultiTenantHttpMcpServer(opts) {
111
115
  authToken: opts.authToken,
112
116
  resolveProvider: opts.resolveProvider,
113
117
  projectRoot: opts.projectRoot,
118
+ instructions: opts.instructions,
114
119
  sessionFingerprint: opts.sessionFingerprint,
115
120
  sessions
116
121
  });
@@ -161,7 +166,8 @@ async function handleMultiTenantRequest(req, res, ctx) {
161
166
  const fingerprint = ctx.sessionFingerprint?.(req);
162
167
  const mcp = createServer({
163
168
  provider,
164
- projectRoot: ctx.projectRoot
169
+ projectRoot: ctx.projectRoot,
170
+ ...ctx.instructions === void 0 ? {} : { instructions: ctx.instructions }
165
171
  });
166
172
  const transport = new StreamableHTTPServerTransport({
167
173
  sessionIdGenerator: () => randomUUID(),
@@ -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 /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */\n instructions?: 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 /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */\n instructions?: 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 /** MCP `instructions` sent at `initialize` (`CreateServerOptions.instructions`); omitted: the server's default for its tools. */\n instructions?: 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({ projectRoot: opts.projectRoot, ...(opts.instructions === undefined ? {} : { instructions: opts.instructions }) })\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, ...(opts.instructions === undefined ? {} : { instructions: opts.instructions }) })\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 instructions: opts.instructions,\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 instructions?: 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, ...(ctx.instructions === undefined ? {} : { instructions: ctx.instructions }) })\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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"}
@@ -3095,15 +3095,25 @@ function registerMediaTools(server, provider, _projectRoot) {
3095
3095
  }
3096
3096
  //#endregion
3097
3097
  //#region package.json
3098
- var version = "3.11.10";
3098
+ var version = "3.11.12";
3099
3099
  //#endregion
3100
3100
  //#region src/server.ts
3101
3101
  /**
3102
- * Default MCP `instructions` surfaced to clients at initialize time.
3103
- * Deliberately kept under 512 characters — directory listings and client
3104
- * UIs truncate longer strings. Override via `CreateServerOptions.instructions`.
3102
+ * Default MCP `instructions` for a server whose tools are `available`.
3103
+ * A hint names only a tool the session registers: `contentrain_apply` is
3104
+ * `projectRoot`-gated, so a remote session (Studio's `/api/mcp/remote`)
3105
+ * is never told to preview it. Deliberately kept under 512 characters —
3106
+ * directory listings and client UIs truncate longer strings.
3105
3107
  */
3106
- const DEFAULT_INSTRUCTIONS = "Contentrain is git-native content governance: models define structure; content is canonical JSON/Markdown on a dedicated branch. Start with contentrain_status; call contentrain_describe_format before creating models or content, and contentrain_describe before editing a model's entries. Every write lands on an isolated cr/* branch and content_save validates before committing. Destructive tools require confirm:true; preview contentrain_apply with dry_run:true first. Content decisions stay with the agent.";
3108
+ function defaultInstructions(available) {
3109
+ return "Contentrain is git-native content governance: models define structure; content is canonical JSON/Markdown on a dedicated branch. Start with contentrain_status; call contentrain_describe_format before creating models or content, and contentrain_describe before editing a model's entries. Every write lands on an isolated cr/* branch and content_save validates before committing. Destructive tools require confirm:true" + (available("contentrain_apply") ? "; preview contentrain_apply with dry_run:true first. " : ". ") + "Content decisions stay with the agent.";
3110
+ }
3111
+ /**
3112
+ * The instructions of a server with every tool registered (local stdio and
3113
+ * CLI flows). Override via `CreateServerOptions.instructions`; when omitted,
3114
+ * `createServer` sends `defaultInstructions` for its own tool surface.
3115
+ */
3116
+ const DEFAULT_INSTRUCTIONS = defaultInstructions(() => true);
3107
3117
  /**
3108
3118
  * `McpServer` variant that silently skips registration for tools named in
3109
3119
  * `skipTools`. Register functions call `server.tool(...)` unconditionally;
@@ -3126,10 +3136,11 @@ var CapabilityFilteredMcpServer = class extends McpServer {
3126
3136
  function createServer(input) {
3127
3137
  const { provider, projectRoot, instructions } = resolveServerContext(input);
3128
3138
  const skipTools = new Set(Object.keys(TOOL_REQUIREMENTS).filter((name) => !isToolAvailable(name, provider, projectRoot)));
3139
+ const sent = instructions ?? defaultInstructions((name) => !skipTools.has(name));
3129
3140
  const server = new CapabilityFilteredMcpServer({
3130
3141
  name: "contentrain-mcp",
3131
3142
  version
3132
- }, { instructions: instructions || void 0 }, skipTools);
3143
+ }, { instructions: sent || void 0 }, skipTools);
3133
3144
  registerContextTools(server, provider, projectRoot);
3134
3145
  registerSetupTools(server, provider, projectRoot);
3135
3146
  registerModelTools(server, provider, projectRoot);
@@ -3147,9 +3158,9 @@ function resolveServerContext(input) {
3147
3158
  if (typeof input === "string") return {
3148
3159
  provider: new LocalProvider(input),
3149
3160
  projectRoot: input,
3150
- instructions: DEFAULT_INSTRUCTIONS
3161
+ instructions: void 0
3151
3162
  };
3152
- const instructions = input.instructions ?? "Contentrain is git-native content governance: models define structure; content is canonical JSON/Markdown on a dedicated branch. Start with contentrain_status; call contentrain_describe_format before creating models or content, and contentrain_describe before editing a model's entries. Every write lands on an isolated cr/* branch and content_save validates before committing. Destructive tools require confirm:true; preview contentrain_apply with dry_run:true first. Content decisions stay with the agent.";
3163
+ const instructions = input.instructions;
3153
3164
  if (input.provider) {
3154
3165
  let projectRoot = input.projectRoot;
3155
3166
  if (!projectRoot) {
@@ -3170,6 +3181,6 @@ function resolveServerContext(input) {
3170
3181
  throw new Error("createServer: either `provider` or `projectRoot` must be provided");
3171
3182
  }
3172
3183
  //#endregion
3173
- export { createServer as n, DEFAULT_INSTRUCTIONS as t };
3184
+ export { createServer as n, defaultInstructions as r, DEFAULT_INSTRUCTIONS as t };
3174
3185
 
3175
- //# sourceMappingURL=server-BI_lvugX.mjs.map
3186
+ //# sourceMappingURL=server-lRjGDFMw.mjs.map