New:Socket for Asana Is Now Available.Learn more
Get Started

@archstone/runtime

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@archstone/runtime - npm Package Compare versions

Comparing version
0.6.0
to
0.7.0
+143
dist/chunk-2A7FY2KF.js
// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import {
inputJsonSchema,
objectJsonSchema,
applyResponseMapping,
contractViolationMessage,
evaluatePolicy,
auditNow,
buildExecutionRecord,
emitExecutionRecord,
LIFECYCLE_BLOCKED_REASON
} from "@archstone/emitter-support";
import { invokeRest } from "@archstone/provider-rest";
function toolDefinitions(registry) {
const resources = registry.ir.resources;
return registry.invocableTools().filter(({ tool: t }) => registry.getExposure(t.id).listed).map(({ name, tool: t }) => {
const hint = registry.getExposure(t.id).hint;
const def = {
name,
description: hint ? `${t.description} (${hint.text})` : t.description,
inputSchema: inputJsonSchema(t.input, resources)
};
if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);
return def;
});
}
var CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
var LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
var POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
async function callTool(registry, name, args, opts) {
const tool = registry.getCapability(name);
if (!tool) {
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
}
const auditSink = opts?.auditSink;
const startedAt = auditSink ? auditNow() : "";
const audit = (status) => {
if (!auditSink) return;
emitExecutionRecord(
auditSink,
buildExecutionRecord({
tool,
input: args,
// Fixed by this call site, never host-configurable: an auditor must be able to trust
// that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same
// path and therefore also records `mcp` — the value names the protocol surface the call
// arrived on, not the npm package that mounted it.
consumer: "mcp",
caller: opts?.caller,
sessionId: opts?.sessionId,
workflowId: opts?.workflowId,
startedAt,
status
})
);
};
const exposure = registry.getExposure(tool.id);
if (!exposure.invocable) {
const text = `capability '${tool.id}' is retired and can no longer be invoked.`;
audit({ phase: "denied", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });
return {
content: [{ type: "text", text }],
_meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: "lifecycle_blocked", capability: tool.id, lifecycle: tool.lifecycle } },
isError: true
};
}
const decision = evaluatePolicy(tool, {
principal: opts?.caller?.principal,
credentialPresent: opts?.caller?.accessToken !== void 0
});
if (!decision.allowed) {
audit({ phase: "denied", message: decision.denial.message, denialReason: decision.denial.reason });
return {
content: [{ type: "text", text: decision.denial.message }],
_meta: {
[POLICY_DENIED_META_KEY]: {
error: "policy_denied",
// `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`
// lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).
capability: tool.id,
reason: decision.denial.reason
}
},
isError: true
};
}
const result = await invokeRest(tool, args, opts);
if (!result.ok) {
const text = result.error ?? "invocation failed";
audit({ phase: "failed", message: text });
return { content: [{ type: "text", text }], isError: true };
}
if (tool.response) {
const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);
if (mapped.status === "violation") {
const missing = mapped.missing ?? [];
const text = contractViolationMessage(tool.id, missing);
audit({ phase: "failed", message: text });
return {
content: [{ type: "text", text }],
_meta: { [CONTRACT_VIOLATION_META_KEY]: { error: "contract_violation", capability: tool.id, missing } },
isError: true
};
}
const content = [{ type: "text", text: JSON.stringify(mapped.data, null, 2) }];
if (mapped.status === "degraded") {
content.push({ type: "text", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(", ")}` });
}
audit({ phase: "succeeded" });
return { content, structuredContent: mapped.data, isError: false };
}
const out = { content: [{ type: "text", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };
if (tool.output.length > 0) {
const data = result.data;
if (data && typeof data === "object" && !Array.isArray(data)) {
out.structuredContent = data;
}
}
audit({ phase: "succeeded" });
return out;
}
function createMcpServer(registry, opts) {
const server = new Server({ name: "archstone", version: "0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const args = req.params.arguments ?? {};
const result = await callTool(registry, req.params.name, args, opts);
return result;
});
return server;
}
export {
toolDefinitions,
CONTRACT_VIOLATION_META_KEY,
LIFECYCLE_BLOCKED_META_KEY,
POLICY_DENIED_META_KEY,
callTool,
createMcpServer
};
//# sourceMappingURL=chunk-2A7FY2KF.js.map
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["// @archstone/runtime — MCP server construction (fs-free)\n//\n// Builds an MCP Server from a Registry and routes invocations through the REST provider\n// (#6). This is the ONLY place the MCP SDK appears (alongside stdio's transport wiring in\n// mcp.ts and the /http subpath's transport wiring in http.ts) — semantic-type → JSON-Schema\n// lowering itself now lives in @archstone/emitter-support (ADD-0008 #27), never here.\n//\n// Extracted out of mcp.ts (ADD-0008 #27) specifically so this module's graph never reaches\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) — only stdio's\n// `serveStdio` (mcp.ts) needs disk access. `http.ts` (the /http subpath) imports only this\n// file, so a consumer depending on that subpath alone stays fs-free.\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n Registry,\n inputJsonSchema,\n objectJsonSchema,\n applyResponseMapping,\n contractViolationMessage,\n evaluatePolicy,\n auditNow,\n buildExecutionRecord,\n emitExecutionRecord,\n LIFECYCLE_BLOCKED_REASON,\n type ExecutionStatus,\n} from \"@archstone/emitter-support\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\n\ntype JsonSchema = Record<string, unknown>;\n\nexport interface McpToolDef {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n}\n\n/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's\n * `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function\n * (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two\n * consistent. Input and output fields lower against the IR resource registry, so a\n * `collection: Stay` output emits a typed, described `outputSchema` (not a bare\n * `{type:object}`).\n *\n * ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional\n * health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned\n * list entirely — unlisted, per D-10, though `experimental` remains callable by id (see\n * `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has\n * its text appended to `description` — the only MCP-specific rendering of the neutral\n * exposure the emitter-support layer computed. */\nexport function toolDefinitions(registry: Registry): McpToolDef[] {\n const resources = registry.ir.resources;\n return registry\n .invocableTools()\n .filter(({ tool: t }) => registry.getExposure(t.id).listed)\n .map(({ name, tool: t }) => {\n const hint = registry.getExposure(t.id).hint;\n const def: McpToolDef = {\n name,\n description: hint ? `${t.description} (${hint.text})` : t.description,\n inputSchema: inputJsonSchema(t.input, resources),\n };\n if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);\n return def;\n });\n}\n\nexport interface CallResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: Record<string, unknown>;\n isError: boolean;\n _meta?: Record<string, unknown>;\n}\n\n/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error\n * object is carried under. Never populated on `structuredContent` (D-3′) — the reference\n * MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool\n * declares one, regardless of `isError`, so a non-conforming error object there crashes the\n * client. `_meta` is untouched by that validation and passes through the client's zod parse\n * unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */\nexport const CONTRACT_VIOLATION_META_KEY = \"dev.archstone/contract_violation\";\n\n/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)\n * tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent\n * (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's\n * unconditional `structuredContent`-against-`outputSchema` validation never sees it. A\n * distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes \"this call was\n * blocked before any connector work\" from \"the provider's response violated the contract\". */\nexport const LIFECYCLE_BLOCKED_META_KEY = \"dev.archstone/lifecycle_blocked\";\n\n/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of\n * the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the\n * reference SDK client validates that against the tool's `outputSchema` unconditionally (not\n * gated on `isError`) and an error object there crashes it. A distinct key from the other two\n * so a client can tell \"refused by policy before any connector work\" from \"blocked by\n * lifecycle\" from \"the provider's response violated the contract\" — the three are mutually\n * exclusive on one call (BR-27). The object discloses the reason code and the capability id\n * and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other\n * principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */\nexport const POLICY_DENIED_META_KEY = \"dev.archstone/policy_denied\";\n\n/** Route an MCP tool call to the REST provider and format the result as MCP content. */\nexport async function callTool(\n registry: Registry,\n name: string,\n args: Record<string, unknown>,\n opts?: InvokeOptions,\n): Promise<CallResult> {\n const tool = registry.getCapability(name);\n if (!tool) {\n // #44: NO audit record. `metadata.capabilityId` is required and the only value available\n // here is an unvalidated, caller-chosen string that is not a CDL id — writing it there\n // would put unbounded attacker-controlled values into the audit log's primary correlation\n // key, in a file a compliance process treats as evidence. An `Execution` record audits a\n // capability invocation attempt; a call naming no capability is a protocol-level event and\n // belongs to the host's own mount-point instrumentation. Named residual, deliberately\n // accepted: tool-name probing (and the collision defence, which also lands here) is\n // invisible to the audit trail.\n return { content: [{ type: \"text\", text: `unknown tool: ${name}` }], isError: true };\n }\n\n // #44: the attempt clock starts HERE — before the exposure gate and before policy evaluation,\n // so every refused attempt still carries a real `startedAt` and can be placed on a timeline.\n // With no sink configured this is a strict no-op: no clock read, no id, no record, no\n // allocation, and every result byte-for-byte what it was before this increment.\n const auditSink = opts?.auditSink;\n const startedAt = auditSink ? auditNow() : \"\";\n const audit = (status: ExecutionStatus): void => {\n if (!auditSink) return;\n emitExecutionRecord(\n auditSink,\n buildExecutionRecord({\n tool,\n input: args,\n // Fixed by this call site, never host-configurable: an auditor must be able to trust\n // that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same\n // path and therefore also records `mcp` — the value names the protocol surface the call\n // arrived on, not the npm package that mounted it.\n consumer: \"mcp\",\n caller: opts?.caller,\n sessionId: opts?.sessionId,\n workflowId: opts?.workflowId,\n startedAt,\n status,\n }),\n );\n };\n\n // ADD-24 D-10/D-11: only `lifecycle: retired` sets `invocable:false` (health never does,\n // D-9) — checked immediately after resolution, before any connector/response work, same\n // call-site discipline the `contract_violation` check already uses downstream.\n const exposure = registry.getExposure(tool.id);\n if (!exposure.invocable) {\n const text = `capability '${tool.id}' is retired and can no longer be invoked.`;\n // #44: `denied`, never `failed` — a refusal by lifecycle is a refusal by governance, and\n // recording it as a failure would conflate it with \"the backend broke\" in the one log where\n // that distinction is the entire product. This gate is the SECOND (and only other) producer\n // of `phase: \"denied\"`, and the reason code the policy evaluator can never return. It runs\n // before policy deliberately, so a capability that is both retired and policy-denied records\n // `lifecycle_blocked`, matching the pinned gate order.\n audit({ phase: \"denied\", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: \"lifecycle_blocked\", capability: tool.id, lifecycle: tool.lifecycle } },\n isError: true,\n };\n }\n\n // #43 (ADD-43 D-5/D-6): THE policy evaluation point, called unconditionally — for every tool,\n // including one with no resolved policy, because `authenticated` enforcement now lives here\n // rather than inside `invokeRest` (D-4). Deliberately AFTER the ADD-24 exposure gate above, so\n // a `retired` capability reports `lifecycle_blocked` and never `policy_denied` (BR-34), and\n // strictly BEFORE `invokeRest`, so a denial does zero connector work: no env/caller\n // resolution, no URL building, no fetch, and no `onResponse` firing (BR-25).\n const decision = evaluatePolicy(tool, {\n principal: opts?.caller?.principal,\n credentialPresent: opts?.caller?.accessToken !== undefined,\n });\n if (!decision.allowed) {\n // #44: the evaluator's OWN reason code, copied verbatim — no re-spelling, no mapping table,\n // no superset for the policy case. The message is likewise the evaluator's, unaltered:\n // the record copies, it never authors.\n audit({ phase: \"denied\", message: decision.denial.message, denialReason: decision.denial.reason });\n return {\n content: [{ type: \"text\", text: decision.denial.message }],\n _meta: {\n [POLICY_DENIED_META_KEY]: {\n error: \"policy_denied\",\n // `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`\n // lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).\n capability: tool.id,\n reason: decision.denial.reason,\n },\n },\n isError: true,\n };\n }\n\n const result = await invokeRest(tool, args, opts);\n if (!result.ok) {\n // #44: every attempt that never completed a usable round-trip — unbound capability, missing\n // env var, missing caller credential, no baseUrl, an allowlist rejection, a missing path\n // parameter, a network error, a non-2xx response — records `failed` carrying the shipped\n // error text verbatim, so a deployer greps the audit log and finds the same string the\n // agent was shown.\n const text = result.error ?? \"invocation failed\";\n audit({ phase: \"failed\", message: text });\n return { content: [{ type: \"text\", text }], isError: true };\n }\n\n // #12 (ADD-12): a binding with a `response:` mapping is now MAPPED + VALIDATED against the\n // resource — the outputSchema (ADD-11) becomes an enforced contract, not just declared.\n if (tool.response) {\n const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);\n if (mapped.status === \"violation\") {\n // Fail closed (D-6): the declared output shape was not met — no raw pass-through.\n const missing = mapped.missing ?? [];\n // The text is unchanged byte-for-byte; it moved into a shared helper (#44) only so the\n // embedded consumer, whose own result carries no text, records the identical sentence.\n const text = contractViolationMessage(tool.id, missing);\n // #19 (ADD-19 Rev 2 D-3′/D-6): structured error object lives in `_meta`, never\n // `structuredContent` — the reference SDK client validates `structuredContent` against\n // the tool's `outputSchema` unconditionally (not gated on `isError`), so a VIOLATION\n // object there (which never conforms to the success outputSchema) crashes the client\n // (verified live against the SDK's own InMemoryTransport, R2.0/R2.2). `capability` is\n // `tool.id`, the unsanitized CDL id — never the MCP-sanitized `name` lookup key (BR-7).\n // #44: a VIOLATION is `failed` — the declared output shape was not met.\n audit({ phase: \"failed\", message: text });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [CONTRACT_VIOLATION_META_KEY]: { error: \"contract_violation\", capability: tool.id, missing } },\n isError: true,\n };\n }\n const content: CallResult[\"content\"] = [{ type: \"text\", text: JSON.stringify(mapped.data, null, 2) }];\n if (mapped.status === \"degraded\") {\n content.push({ type: \"text\", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(\", \")}` });\n }\n // #44: `degraded` records `succeeded`, NOT `failed` — every *required* field was present and\n // an optional one was not, so the invocation succeeded. Pinned in a comment because\n // \"degraded\" reads like a failure and the next reader will guess otherwise.\n audit({ phase: \"succeeded\" });\n return { content, structuredContent: mapped.data, isError: false };\n }\n\n // No response mapping: today's raw pass-through (rollout-safe). The declared outputSchema is\n // NOT yet enforced for these tools — add a `response:` block to close the loop (ADD-12 R-3).\n const out: CallResult = { content: [{ type: \"text\", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };\n if (tool.output.length > 0) {\n const data = result.data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n out.structuredContent = data as Record<string, unknown>;\n }\n }\n // #44: `status.output` is deliberately NOT populated here (nor anywhere) — `result.data` is\n // exactly the payload the record must never carry.\n audit({ phase: \"succeeded\" });\n return out;\n}\n\n/** Build an MCP Server that lists and invokes the registry's tools. */\nexport function createMcpServer(registry: Registry, opts?: InvokeOptions): Server {\n const server = new Server({ name: \"archstone\", version: \"0\" }, { capabilities: { tools: {} } });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const args = (req.params.arguments ?? {}) as Record<string, unknown>;\n const result = await callTool(registry, req.params.name, args, opts);\n return result as CallToolResult;\n });\n\n return server;\n}\n"],"mappings":"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","names":[]}
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions } from '@archstone/provider-rest';
type JsonSchema = Record<string, unknown>;
interface McpToolDef {
name: string;
description: string;
inputSchema: JsonSchema;
outputSchema?: JsonSchema;
}
/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's
* `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function
* (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two
* consistent. Input and output fields lower against the IR resource registry, so a
* `collection: Stay` output emits a typed, described `outputSchema` (not a bare
* `{type:object}`).
*
* ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional
* health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned
* list entirely — unlisted, per D-10, though `experimental` remains callable by id (see
* `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has
* its text appended to `description` — the only MCP-specific rendering of the neutral
* exposure the emitter-support layer computed. */
declare function toolDefinitions(registry: Registry): McpToolDef[];
interface CallResult {
content: {
type: "text";
text: string;
}[];
structuredContent?: Record<string, unknown>;
isError: boolean;
_meta?: Record<string, unknown>;
}
/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error
* object is carried under. Never populated on `structuredContent` (D-3′) — the reference
* MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool
* declares one, regardless of `isError`, so a non-conforming error object there crashes the
* client. `_meta` is untouched by that validation and passes through the client's zod parse
* unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */
declare const CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)
* tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent
* (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's
* unconditional `structuredContent`-against-`outputSchema` validation never sees it. A
* distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes "this call was
* blocked before any connector work" from "the provider's response violated the contract". */
declare const LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of
* the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the
* reference SDK client validates that against the tool's `outputSchema` unconditionally (not
* gated on `isError`) and an error object there crashes it. A distinct key from the other two
* so a client can tell "refused by policy before any connector work" from "blocked by
* lifecycle" from "the provider's response violated the contract" — the three are mutually
* exclusive on one call (BR-27). The object discloses the reason code and the capability id
* and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other
* principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */
declare const POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
/** Route an MCP tool call to the REST provider and format the result as MCP content. */
declare function callTool(registry: Registry, name: string, args: Record<string, unknown>, opts?: InvokeOptions): Promise<CallResult>;
/** Build an MCP Server that lists and invokes the registry's tools. */
declare function createMcpServer(registry: Registry, opts?: InvokeOptions): Server;
export { CONTRACT_VIOLATION_META_KEY as C, LIFECYCLE_BLOCKED_META_KEY as L, type McpToolDef as M, POLICY_DENIED_META_KEY as P, type CallResult as a, callTool as b, createMcpServer as c, toolDefinitions as t };
+9
-2
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions, CallerContext } from '@archstone/provider-rest';
export { c as createMcpServer } from './server-DXSou3Bi.js';
export { c as createMcpServer } from './server-Cqvs__bv.js';
import '@modelcontextprotocol/sdk/server/index.js';

@@ -13,3 +13,10 @@

/** Forwarded to createMcpServer for REST-provider calls (env/fetchImpl). A `caller` set here
* is a static, process-wide default — see `resolveCaller` below for the per-request case. */
* is a static, process-wide default — see `resolveCaller` below for the per-request case.
*
* #44, and read this before setting a correlation id here: the per-request rebuild below
* overwrites exactly ONE key, `caller`. Everything else — including `auditSink`,
* `sessionId` and `workflowId` — survives the spread. That is what makes an audit sink set
* here work at all, and it is also the trap: a `sessionId` set here silently stamps every
* concurrent request with the same session, where a `caller` set here fails loudly instead.
* There is no per-request correlation seam on this surface today. */
invoke?: InvokeOptions;

@@ -16,0 +23,0 @@ /**

+1
-1
import {
createMcpServer
} from "./chunk-C6DASWPB.js";
} from "./chunk-2A7FY2KF.js";

@@ -5,0 +5,0 @@ // src/http.ts

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/http.ts"],"sourcesContent":["// @archstone/runtime/http — Web-standard Streamable-HTTP transport (ADD-0008 #27)\n//\n// createMcpServer (fs-free, from ./server) + createHttpHandler, the one Streamable-HTTP\n// implementation D-3 asks for — shared by `archstone serve --http` and\n// @archstone/agent's `mcpHandler()` (both #29). Nothing reachable from this module imports\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) or node:fs/node:path —\n// a consumer depending on this subpath alone stays fs-free without relying on a bundler's\n// nodejs_compat-style flag.\n\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport type { Registry } from \"@archstone/emitter-support\";\nimport type { CallerContext, InvokeOptions } from \"@archstone/provider-rest\";\nimport { createMcpServer } from \"./server\";\n\nexport { createMcpServer } from \"./server\";\n\nexport interface CreateHttpHandlerOptions {\n /** Required shared secret gating access to the MCP protocol surface — who may reach\n * `initialize`/`tools/list`/`tools/call` on this endpoint at all (ADD-0008 §5). Missing or\n * empty throws at construction time, not on the first request (Rule #7 — core never ships\n * open by default; R-5). */\n bearerToken: string;\n /** Forwarded to createMcpServer for REST-provider calls (env/fetchImpl). A `caller` set here\n * is a static, process-wide default — see `resolveCaller` below for the per-request case. */\n invoke?: InvokeOptions;\n /**\n * ADD-32: extracts the caller credential for ONE inbound request. Called inside the\n * per-request handler closure (a fresh MCP server is already built per request here, so\n * this varies per call, unlike `invoke.caller` above which is fixed at construction time).\n * Archstone does not validate the token itself — this is a seam for a host that has\n * *already* authenticated its end user and is handing over the resulting token; Archstone\n * does not host an OIDC broker.\n *\n * Orthogonal to `bearerToken` (R-2) — do not conflate the two:\n * - `bearerToken` gates WHO may reach this MCP endpoint at all (endpoint access).\n * - `resolveCaller` resolves WHOSE backend data a given, already-authorized call acts on.\n * They compose (both may be set); neither substitutes for the other. A request can be a\n * validly-authorized MCP client (passed `bearerToken`) yet still supply no/invalid caller\n * credential, which then fails closed inside `invokeRest` for any `authenticated` capability.\n */\n resolveCaller?: (request: Request) => CallerContext | undefined;\n}\n\n/**\n * A mountable, Web-standard `(Request) => Promise<Response>` MCP endpoint, bearer-token\n * gated. A missing or wrong `Authorization: Bearer` header gets a bare 401 — no tool\n * information in the body. No CORS headers are set: intended callers (Claude API\n * `mcp_servers`, ChatGPT connectors) are server-to-server, not browser `fetch` (ADD-0008 §5).\n */\nexport function createHttpHandler(\n registry: Registry,\n opts: CreateHttpHandlerOptions,\n): (request: Request) => Promise<Response> {\n if (!opts.bearerToken) {\n throw new Error(\"createHttpHandler: bearerToken is required and must be non-empty\");\n }\n const expected = `Bearer ${opts.bearerToken}`;\n\n return async (request: Request): Promise<Response> => {\n if (request.headers.get(\"authorization\") !== expected) {\n return new Response(null, { status: 401 });\n }\n\n // Per-request InvokeOptions: opts.invoke's env/fetchImpl carry over unchanged; `caller` is\n // resolved fresh for THIS request via resolveCaller (ADD-32) — never cached across requests.\n const invoke: InvokeOptions = { ...opts.invoke, caller: opts.resolveCaller?.(request) };\n const server = createMcpServer(registry, invoke);\n // Stateless: no sessionIdGenerator, no per-caller session/state at all. JSON responses\n // (not SSE) — a freshly-built server per request has nothing to stream anyway.\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n await server.connect(transport);\n return transport.handleRequest(request);\n };\n}\n"],"mappings":";;;;;AASA,SAAS,gDAAgD;AAwClD,SAAS,kBACd,UACA,MACyC;AACzC,MAAI,CAAC,KAAK,aAAa;AACrB,UAAM,IAAI,MAAM,kEAAkE;AAAA,EACpF;AACA,QAAM,WAAW,UAAU,KAAK,WAAW;AAE3C,SAAO,OAAO,YAAwC;AACpD,QAAI,QAAQ,QAAQ,IAAI,eAAe,MAAM,UAAU;AACrD,aAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C;AAIA,UAAM,SAAwB,EAAE,GAAG,KAAK,QAAQ,QAAQ,KAAK,gBAAgB,OAAO,EAAE;AACtF,UAAM,SAAS,gBAAgB,UAAU,MAAM;AAG/C,UAAM,YAAY,IAAI,yCAAyC;AAAA,MAC7D,oBAAoB;AAAA,MACpB,oBAAoB;AAAA,IACtB,CAAC;AACD,UAAM,OAAO,QAAQ,SAAS;AAC9B,WAAO,UAAU,cAAc,OAAO;AAAA,EACxC;AACF;","names":[]}
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["// @archstone/runtime/http — Web-standard Streamable-HTTP transport (ADD-0008 #27)\n//\n// createMcpServer (fs-free, from ./server) + createHttpHandler, the one Streamable-HTTP\n// implementation D-3 asks for — shared by `archstone serve --http` and\n// @archstone/agent's `mcpHandler()` (both #29). Nothing reachable from this module imports\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) or node:fs/node:path —\n// a consumer depending on this subpath alone stays fs-free without relying on a bundler's\n// nodejs_compat-style flag.\n\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport type { Registry } from \"@archstone/emitter-support\";\nimport type { CallerContext, InvokeOptions } from \"@archstone/provider-rest\";\nimport { createMcpServer } from \"./server\";\n\nexport { createMcpServer } from \"./server\";\n\nexport interface CreateHttpHandlerOptions {\n /** Required shared secret gating access to the MCP protocol surface — who may reach\n * `initialize`/`tools/list`/`tools/call` on this endpoint at all (ADD-0008 §5). Missing or\n * empty throws at construction time, not on the first request (Rule #7 — core never ships\n * open by default; R-5). */\n bearerToken: string;\n /** Forwarded to createMcpServer for REST-provider calls (env/fetchImpl). A `caller` set here\n * is a static, process-wide default — see `resolveCaller` below for the per-request case.\n *\n * #44, and read this before setting a correlation id here: the per-request rebuild below\n * overwrites exactly ONE key, `caller`. Everything else — including `auditSink`,\n * `sessionId` and `workflowId` — survives the spread. That is what makes an audit sink set\n * here work at all, and it is also the trap: a `sessionId` set here silently stamps every\n * concurrent request with the same session, where a `caller` set here fails loudly instead.\n * There is no per-request correlation seam on this surface today. */\n invoke?: InvokeOptions;\n /**\n * ADD-32: extracts the caller credential for ONE inbound request. Called inside the\n * per-request handler closure (a fresh MCP server is already built per request here, so\n * this varies per call, unlike `invoke.caller` above which is fixed at construction time).\n * Archstone does not validate the token itself — this is a seam for a host that has\n * *already* authenticated its end user and is handing over the resulting token; Archstone\n * does not host an OIDC broker.\n *\n * Orthogonal to `bearerToken` (R-2) — do not conflate the two:\n * - `bearerToken` gates WHO may reach this MCP endpoint at all (endpoint access).\n * - `resolveCaller` resolves WHOSE backend data a given, already-authorized call acts on.\n * They compose (both may be set); neither substitutes for the other. A request can be a\n * validly-authorized MCP client (passed `bearerToken`) yet still supply no/invalid caller\n * credential, which then fails closed inside `invokeRest` for any `authenticated` capability.\n */\n resolveCaller?: (request: Request) => CallerContext | undefined;\n}\n\n/**\n * A mountable, Web-standard `(Request) => Promise<Response>` MCP endpoint, bearer-token\n * gated. A missing or wrong `Authorization: Bearer` header gets a bare 401 — no tool\n * information in the body. No CORS headers are set: intended callers (Claude API\n * `mcp_servers`, ChatGPT connectors) are server-to-server, not browser `fetch` (ADD-0008 §5).\n */\nexport function createHttpHandler(\n registry: Registry,\n opts: CreateHttpHandlerOptions,\n): (request: Request) => Promise<Response> {\n if (!opts.bearerToken) {\n throw new Error(\"createHttpHandler: bearerToken is required and must be non-empty\");\n }\n const expected = `Bearer ${opts.bearerToken}`;\n\n return async (request: Request): Promise<Response> => {\n if (request.headers.get(\"authorization\") !== expected) {\n return new Response(null, { status: 401 });\n }\n\n // Per-request InvokeOptions: opts.invoke's env/fetchImpl carry over unchanged; `caller` is\n // resolved fresh for THIS request via resolveCaller (ADD-32) — never cached across requests.\n const invoke: InvokeOptions = { ...opts.invoke, caller: opts.resolveCaller?.(request) };\n const server = createMcpServer(registry, invoke);\n // Stateless: no sessionIdGenerator, no per-caller session/state at all. JSON responses\n // (not SSE) — a freshly-built server per request has nothing to stream anyway.\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n await server.connect(transport);\n return transport.handleRequest(request);\n };\n}\n"],"mappings":";;;;;AASA,SAAS,gDAAgD;AA+ClD,SAAS,kBACd,UACA,MACyC;AACzC,MAAI,CAAC,KAAK,aAAa;AACrB,UAAM,IAAI,MAAM,kEAAkE;AAAA,EACpF;AACA,QAAM,WAAW,UAAU,KAAK,WAAW;AAE3C,SAAO,OAAO,YAAwC;AACpD,QAAI,QAAQ,QAAQ,IAAI,eAAe,MAAM,UAAU;AACrD,aAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C;AAIA,UAAM,SAAwB,EAAE,GAAG,KAAK,QAAQ,QAAQ,KAAK,gBAAgB,OAAO,EAAE;AACtF,UAAM,SAAS,gBAAgB,UAAU,MAAM;AAG/C,UAAM,YAAY,IAAI,yCAAyC;AAAA,MAC7D,oBAAoB;AAAA,MACpB,oBAAoB;AAAA,IACtB,CAAC;AACD,UAAM,OAAO,QAAQ,SAAS;AAC9B,WAAO,UAAU,cAAc,OAAO;AAAA,EACxC;AACF;","names":[]}
import { LoadIssue } from '@archstone/schema';
import { Diagnostic, IRTool, IRResourceRegistry } from '@archstone/compiler';
import { Registry, HealthStatus } from '@archstone/emitter-support';
export { HealthStatus, MappingResult, MappingStatus, Registry, applyResponseMapping, inputJsonSchema, objectJsonSchema, toolName } from '@archstone/emitter-support';
export { AuditSink, AuditWritable, ExecutionConsumer, ExecutionDenialReason, ExecutionPhase, ExecutionRecord, ExecutionStatus, HealthStatus, LIFECYCLE_BLOCKED_REASON, MappingResult, MappingStatus, REDACTED, Registry, applyResponseMapping, inputJsonSchema, jsonLinesAuditSink, objectJsonSchema, toolName } from '@archstone/emitter-support';
import { InvokeOptions } from '@archstone/provider-rest';
export { C as CONTRACT_VIOLATION_META_KEY, a as CallResult, L as LIFECYCLE_BLOCKED_META_KEY, M as McpToolDef, b as callTool, c as createMcpServer, t as toolDefinitions } from './server-DXSou3Bi.js';
export { C as CONTRACT_VIOLATION_META_KEY, a as CallResult, L as LIFECYCLE_BLOCKED_META_KEY, M as McpToolDef, P as POLICY_DENIED_META_KEY, b as callTool, c as createMcpServer, t as toolDefinitions } from './server-Cqvs__bv.js';
import '@modelcontextprotocol/sdk/server/index.js';

@@ -47,2 +47,29 @@

detail: string;
/**
* #43 (ADD-43 D-14): set iff this verification was refused by the policy evaluation point
* before any request was issued — i.e. the probe observed **nothing at all** about the
* backend's contract.
*
* Why an additive optional field rather than a fourth `HealthStatus` value: `HealthStatus` is
* a CLOSED set already consumed by ADD-24's `combineExposure` and by ADD-20's published
* `archstone verify --json` shape, so a `"denied"` member would take a published CLI contract
* and the exposure severity ordering with it. `red` stays correct for the OPERATOR-facing
* report — they asked "is this binding healthy?" and the honest answer is "I could not
* establish that" (D-7).
*
* What this flag exists to stop is that `red` travelling ONWARD into an AGENT-facing surface.
* `readHealthSnapshot` (registry.ts) skips any entry carrying it, so the tool ends up with no
* health entry at all, `combineExposure` leaves its exposure untouched, and no hint is
* appended to its advertised description. Without it, the documented ADD-24 D-8 workflow
* (`archstone verify --json` > `.archstone-health.json`, then serve) would append
* `"binding health: red — the last contract verification failed"` at the highest severity to
* a policy-gated tool's description, for EVERY caller including permitted ones — a statement
* that is factually false (no verification occurred) and that makes policy affect listing,
* which BR-36 forbids. Because the CLI supplies no caller, that is the DEFAULT outcome for
* any `allow`-bearing capability, not a corner case.
*
* The failure is silent: nothing throws, no exit code changes, an agent just reads a false
* warning. `runtime/test/lifecycle.integration.test.ts` asserts it.
*/
policyDenied?: true;
}

@@ -49,0 +76,0 @@ interface GoldenFixture {

import {
CONTRACT_VIOLATION_META_KEY,
LIFECYCLE_BLOCKED_META_KEY,
POLICY_DENIED_META_KEY,
callTool,
createMcpServer,
toolDefinitions
} from "./chunk-C6DASWPB.js";
} from "./chunk-2A7FY2KF.js";

@@ -30,2 +31,3 @@ // src/registry.ts

if (!r || typeof r !== "object") continue;
if (r.policyDenied === true) continue;
const capabilityId = r.capabilityId;

@@ -66,2 +68,3 @@ const status = r.status;

import { toolName, inputJsonSchema, objectJsonSchema } from "@archstone/emitter-support";
import { jsonLinesAuditSink, REDACTED, LIFECYCLE_BLOCKED_REASON } from "@archstone/emitter-support";
async function serveStdio(dir, invoke) {

@@ -89,2 +92,3 @@ const built = buildRegistry(dir);

import { invokeRest } from "@archstone/provider-rest";
import { evaluatePolicy } from "@archstone/emitter-support";
function readFixture(dir, path) {

@@ -103,2 +107,14 @@ try {

if (!fixture) return { ...base, status: "red", detail: `fixture not found or unreadable: ${contract.probeFixture}` };
const decision = evaluatePolicy(tool, {
principal: opts?.caller?.principal,
credentialPresent: opts?.caller?.accessToken !== void 0
});
if (!decision.allowed) {
return {
...base,
status: "red",
detail: `policy denied before any request was made: ${decision.denial.message}`,
policyDenied: true
};
}
const result = await invokeRest(tool, fixture.request, opts);

@@ -137,2 +153,5 @@ if (!result.ok) return { ...base, status: "red", detail: `live request failed: ${result.error ?? `status ${result.status}`}` };

LIFECYCLE_BLOCKED_META_KEY,
LIFECYCLE_BLOCKED_REASON,
POLICY_DENIED_META_KEY,
REDACTED,
Registry2 as Registry,

@@ -144,2 +163,3 @@ applyResponseMapping,

inputJsonSchema,
jsonLinesAuditSink,
objectJsonSchema,

@@ -146,0 +166,0 @@ runVerify,

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/registry.ts","../src/mcp.ts","../src/mapping.ts","../src/verify.ts"],"sourcesContent":["// @archstone/runtime — Capability Registry (#5)\n//\n// The product kernel: capabilities queryable at runtime, indexed over the IR.\n// File-backed (no DB) — the IR is derived from manifests on disk. The MCP emitter\n// (#7) consumes this to list and resolve tools.\n//\n// `Registry` (index-only) moved to @archstone/emitter-support (ADD-0008 #27) — re-exported\n// here for back-compat so nothing downstream breaks. This file keeps the fs-touching\n// pipeline (`buildRegistry`), which is why the /http subpath (http.ts) never imports it.\n//\n// ADD-24 (#24): `buildRegistry` also optionally reads a conventional health-snapshot file\n// (`readHealthSnapshot`, below) — the ONE other fs-touching, network-free addition this ADD\n// makes. Binding health itself is never computed here (that's `archstone verify`'s own live\n// probe, ADD-18 D-5) — only its already-serialized `--json` output is read back.\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { load, type LoadResult, type LoadIssue } from \"@archstone/schema\";\nimport { validateSemantics, compile, type Diagnostic } from \"@archstone/compiler\";\nimport { Registry, type HealthStatus } from \"@archstone/emitter-support\";\n\nexport { Registry } from \"@archstone/emitter-support\";\n\n/** Conventional health-snapshot file, read once next to the manifest dir (ADD-24 D-8): the\n * operator/CI populates it by redirecting the ALREADY-shipped `archstone verify --json`\n * output here — no new serialization. `buildRegistry` reads it (fs, but no network — the\n * live probe stays exclusively `verify`'s, ADD-18 D-5) and hands the parsed map to\n * `Registry`, which composes it with each tool's lifecycle exposure (ADD-24 §7 step 5). */\nexport const HEALTH_SNAPSHOT_FILE = \".archstone-health.json\";\n\nconst HEALTH_STATUSES: ReadonlySet<string> = new Set([\"green\", \"yellow\", \"red\"]);\n\n/**\n * Parse the `{results: ToolVerification[]}` shape `archstone verify --json` already produces\n * (ADD-20) into a capabilityId -> HealthStatus map. Fail-open (ADD-24 D-9): a missing file, a\n * parse error, or a malformed/unexpected shape all return `undefined` — the caller then\n * proceeds with lifecycle-only exposure, never mistaking \"no snapshot\" for \"known bad\".\n */\nfunction readHealthSnapshot(dir: string): Map<string, HealthStatus> | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(join(dir, HEALTH_SNAPSHOT_FILE), \"utf8\"));\n } catch {\n return undefined; // absent, unreadable, or invalid JSON — fail-open\n }\n\n const results = (parsed as { results?: unknown } | null)?.results;\n if (!Array.isArray(results)) return undefined;\n\n const map = new Map<string, HealthStatus>();\n for (const r of results) {\n if (!r || typeof r !== \"object\") continue;\n const capabilityId = (r as { capabilityId?: unknown }).capabilityId;\n const status = (r as { status?: unknown }).status;\n if (typeof capabilityId === \"string\" && typeof status === \"string\" && HEALTH_STATUSES.has(status)) {\n map.set(capabilityId, status as HealthStatus);\n }\n }\n return map;\n}\n\nexport interface BuildResult {\n ok: boolean;\n registry?: Registry;\n issues: LoadIssue[];\n diagnostics: Diagnostic[];\n}\n\n/**\n * File-backed pipeline: load (#2) → semantic-validate (#3) → compile (#4) → Registry (#5).\n * `registry` is present only when shapes are valid, there are no semantic errors, AND no\n * tool-name collision (ADD-30 D-2) — folded into this function's existing `diagnostics`/\n * `ok` contract (new `tool-name-collision` diagnostic code) rather than a new mechanism, so\n * `serveStdio`/`runServeHttp` (which already refuse to proceed on `!built.ok`) inherit the\n * gate for free.\n */\nexport function buildRegistry(dir: string): BuildResult {\n const model: LoadResult = load(dir);\n const diagnostics = validateSemantics(model);\n const hasErrors = diagnostics.some((d) => d.severity === \"error\");\n let ok = model.ok && !hasErrors;\n\n const registry = ok ? new Registry(compile(model), readHealthSnapshot(dir)) : undefined;\n if (registry) {\n for (const c of registry.toolNameCollisions) {\n ok = false;\n diagnostics.push({\n severity: \"error\",\n code: \"tool-name-collision\",\n message: `tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`,\n });\n }\n }\n\n return {\n ok,\n registry: ok ? registry : undefined,\n issues: model.issues,\n diagnostics,\n };\n}\n","// @archstone/runtime — MCP emitter (#7) — stdio entrypoint\n//\n// serveStdio builds the registry from disk (buildRegistry, registry.ts) and serves it over\n// stdio, the channel Claude Desktop uses. The fs-free MCP server construction\n// (toolDefinitions/callTool/createMcpServer) lives in ./server (ADD-0008 #27) — re-exported\n// here, alongside the semantic-lowering functions from @archstone/emitter-support, for\n// back-compat so nothing downstream breaks.\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport type { InvokeOptions } from \"@archstone/provider-rest\";\nimport { buildRegistry } from \"./registry\";\nimport { toolDefinitions, createMcpServer } from \"./server\";\n\nexport { toolName, inputJsonSchema, objectJsonSchema } from \"@archstone/emitter-support\";\nexport * from \"./server\";\n\n/**\n * Build the registry from a manifest dir and serve it over stdio (blocks).\n *\n * `invoke` (ADD-32) is forwarded verbatim to `createMcpServer` — this closes a real gap: prior\n * to #32, `serveStdio` passed NO `InvokeOptions` at all, so nothing (not `env`, not `caller`)\n * could ever be injected here. A stdio server is one child process per conversation (Claude\n * Desktop's model) — single-process, single-user by construction — so a static per-process\n * `invoke.caller` is architecturally sound here, unlike the HTTP case (`createHttpHandler`'s\n * `resolveCaller`, which must vary per inbound request).\n */\nexport async function serveStdio(dir: string, invoke?: InvokeOptions): Promise<void> {\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n // stdout is the MCP channel — all human output goes to stderr.\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n const tools = toolDefinitions(built.registry);\n console.error(`archstone: serving ${tools.length} tool(s) over stdio: ${tools.map((t) => t.name).join(\", \") || \"(none)\"}`);\n const server = createMcpServer(built.registry, invoke);\n await server.connect(new StdioServerTransport());\n}\n","// @archstone/runtime — Response mapper (ADD-12 / RFC-0006)\n//\n// Moved to @archstone/emitter-support (ADD-0008 #27), unchanged logic — re-exported here for\n// back-compat so nothing downstream breaks (verify.ts and existing consumers still import\n// from \"./mapping\").\nexport { applyResponseMapping, type MappingStatus, type MappingResult } from \"@archstone/emitter-support\";\n","// @archstone/runtime — Contract probe runner (ADD-18 / RFC-0006 Phase 2).\n//\n// `runVerify` replays a bound tool's golden fixture against the LIVE backend and\n// derives a health status. This is the only place outside a real MCP invocation that\n// makes a network call — always explicit, on demand (`archstone verify`), never\n// triggered by `apply`/`serve`. Reuses #12's `applyResponseMapping` verbatim (ADD-18\n// D-3/R-4): one mapper, so a probe VIOLATION is exactly what a real call would see.\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fingerprintShape, type IRTool, type IRResourceRegistry } from \"@archstone/compiler\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\nimport { applyResponseMapping } from \"./mapping\";\n// ADD-24: HealthStatus's canonical home moved to @archstone/emitter-support (registry.ts's\n// exposure composition needs it, and runtime depends on emitter-support, never the reverse) —\n// re-exported here, unchanged, so nothing downstream (e.g. the CLI's `HealthStatus` import\n// from \"@archstone/runtime\") breaks.\nimport type { HealthStatus } from \"@archstone/emitter-support\";\nexport type { HealthStatus } from \"@archstone/emitter-support\";\n\nexport interface ToolVerification {\n capabilityId: string;\n status: HealthStatus;\n detail: string;\n}\n\nexport interface GoldenFixture {\n capabilityId: string;\n recordedAt?: string;\n request: Record<string, unknown>;\n expects?: { collectionNonEmpty?: boolean };\n}\n\nfunction readFixture(dir: string, path: string): GoldenFixture | undefined {\n try {\n return JSON.parse(readFileSync(resolve(dir, path), \"utf8\")) as GoldenFixture;\n } catch {\n return undefined;\n }\n}\n\n/** Verify one tool's contract against the live backend. Returns green/yellow/red — never\n * throws (a network/fs failure is itself a red result, not an exception the CLI must catch). */\nexport async function verifyTool(tool: IRTool, dir: string, resources: IRResourceRegistry, opts?: InvokeOptions): Promise<ToolVerification> {\n const base = { capabilityId: tool.id };\n const contract = tool.contract;\n if (!contract) return { ...base, status: \"red\", detail: \"no contract: declared — nothing to verify\" };\n\n const fixture = readFixture(dir, contract.probeFixture);\n if (!fixture) return { ...base, status: \"red\", detail: `fixture not found or unreadable: ${contract.probeFixture}` };\n\n const result = await invokeRest(tool, fixture.request, opts);\n if (!result.ok) return { ...base, status: \"red\", detail: `live request failed: ${result.error ?? `status ${result.status}`}` };\n\n const liveFingerprint = fingerprintShape(result.data);\n const fingerprintChanged = liveFingerprint !== contract.fingerprint;\n\n if (!tool.response) {\n // No response mapping to validate against — fingerprint drift is all we can see.\n return fingerprintChanged\n ? { ...base, status: \"yellow\", detail: `response shape changed (fingerprint ${contract.fingerprint} → ${liveFingerprint})` }\n : { ...base, status: \"green\", detail: \"fingerprint unchanged\" };\n }\n\n const mapped = applyResponseMapping(tool, result.data, resources);\n if (mapped.status === \"violation\") {\n return { ...base, status: \"red\", detail: `contract violation: missing required field(s) ${(mapped.missing ?? []).join(\", \")}` };\n }\n\n if (fixture.expects?.collectionNonEmpty) {\n const field = tool.response.field;\n const value = mapped.data?.[field];\n const empty = Array.isArray(value) ? value.length === 0 : value === undefined || value === null;\n if (empty) return { ...base, status: \"red\", detail: `expected a non-empty '${field}' collection; got none` };\n }\n\n if (mapped.status === \"degraded\") {\n return { ...base, status: \"yellow\", detail: `degraded: optional field(s) absent — ${(mapped.degraded ?? []).join(\", \")}` };\n }\n if (fingerprintChanged) {\n return { ...base, status: \"yellow\", detail: `response shape changed (fingerprint ${contract.fingerprint} → ${liveFingerprint}) but mapping still resolves` };\n }\n return { ...base, status: \"green\", detail: \"fingerprint unchanged, mapping OK\" };\n}\n\n/** Verify every contract-bearing tool in a registry. */\nexport async function runVerify(\n tools: IRTool[],\n dir: string,\n resources: IRResourceRegistry,\n opts?: InvokeOptions,\n): Promise<ToolVerification[]> {\n const contractBearing = tools.filter((t) => t.contract);\n return Promise.all(contractBearing.map((t) => verifyTool(t, dir, resources, opts)));\n}\n"],"mappings":"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","names":["Registry","readFileSync","readFileSync"]}
{"version":3,"sources":["../src/registry.ts","../src/mcp.ts","../src/mapping.ts","../src/verify.ts"],"sourcesContent":["// @archstone/runtime — Capability Registry (#5)\n//\n// The product kernel: capabilities queryable at runtime, indexed over the IR.\n// File-backed (no DB) — the IR is derived from manifests on disk. The MCP emitter\n// (#7) consumes this to list and resolve tools.\n//\n// `Registry` (index-only) moved to @archstone/emitter-support (ADD-0008 #27) — re-exported\n// here for back-compat so nothing downstream breaks. This file keeps the fs-touching\n// pipeline (`buildRegistry`), which is why the /http subpath (http.ts) never imports it.\n//\n// ADD-24 (#24): `buildRegistry` also optionally reads a conventional health-snapshot file\n// (`readHealthSnapshot`, below) — the ONE other fs-touching, network-free addition this ADD\n// makes. Binding health itself is never computed here (that's `archstone verify`'s own live\n// probe, ADD-18 D-5) — only its already-serialized `--json` output is read back.\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { load, type LoadResult, type LoadIssue } from \"@archstone/schema\";\nimport { validateSemantics, compile, type Diagnostic } from \"@archstone/compiler\";\nimport { Registry, type HealthStatus } from \"@archstone/emitter-support\";\n\nexport { Registry } from \"@archstone/emitter-support\";\n\n/** Conventional health-snapshot file, read once next to the manifest dir (ADD-24 D-8): the\n * operator/CI populates it by redirecting the ALREADY-shipped `archstone verify --json`\n * output here — no new serialization. `buildRegistry` reads it (fs, but no network — the\n * live probe stays exclusively `verify`'s, ADD-18 D-5) and hands the parsed map to\n * `Registry`, which composes it with each tool's lifecycle exposure (ADD-24 §7 step 5). */\nexport const HEALTH_SNAPSHOT_FILE = \".archstone-health.json\";\n\nconst HEALTH_STATUSES: ReadonlySet<string> = new Set([\"green\", \"yellow\", \"red\"]);\n\n/**\n * Parse the `{results: ToolVerification[]}` shape `archstone verify --json` already produces\n * (ADD-20) into a capabilityId -> HealthStatus map. Fail-open (ADD-24 D-9): a missing file, a\n * parse error, or a malformed/unexpected shape all return `undefined` — the caller then\n * proceeds with lifecycle-only exposure, never mistaking \"no snapshot\" for \"known bad\".\n *\n * #43 (ADD-43 D-14): an entry marked `policyDenied` is SKIPPED. A refusal that happened before\n * the call is not a health fact — no request was issued, so nothing about the backend's contract\n * was observed. Left in, it would reach `combineExposure` as a `red` and append\n * `\"binding health: red — the last contract verification failed\"` to the tool's agent-facing\n * description at the highest severity: a statement that never happened, shown to every caller\n * including permitted ones, making policy affect listing (which BR-36 forbids). Skipping leaves\n * the tool with NO health entry, which is exactly ADD-24 D-9's ratified posture — absent health\n * must never be manufactured into known-bad.\n */\nfunction readHealthSnapshot(dir: string): Map<string, HealthStatus> | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(join(dir, HEALTH_SNAPSHOT_FILE), \"utf8\"));\n } catch {\n return undefined; // absent, unreadable, or invalid JSON — fail-open\n }\n\n const results = (parsed as { results?: unknown } | null)?.results;\n if (!Array.isArray(results)) return undefined;\n\n const map = new Map<string, HealthStatus>();\n for (const r of results) {\n if (!r || typeof r !== \"object\") continue;\n // ADD-43 D-14: a policy denial is not a health reading — drop it rather than let it become\n // an agent-facing \"the last contract verification failed\" hint for a verification that\n // never ran. See this function's doc comment.\n if ((r as { policyDenied?: unknown }).policyDenied === true) continue;\n const capabilityId = (r as { capabilityId?: unknown }).capabilityId;\n const status = (r as { status?: unknown }).status;\n if (typeof capabilityId === \"string\" && typeof status === \"string\" && HEALTH_STATUSES.has(status)) {\n map.set(capabilityId, status as HealthStatus);\n }\n }\n return map;\n}\n\nexport interface BuildResult {\n ok: boolean;\n registry?: Registry;\n issues: LoadIssue[];\n diagnostics: Diagnostic[];\n}\n\n/**\n * File-backed pipeline: load (#2) → semantic-validate (#3) → compile (#4) → Registry (#5).\n * `registry` is present only when shapes are valid, there are no semantic errors, AND no\n * tool-name collision (ADD-30 D-2) — folded into this function's existing `diagnostics`/\n * `ok` contract (new `tool-name-collision` diagnostic code) rather than a new mechanism, so\n * `serveStdio`/`runServeHttp` (which already refuse to proceed on `!built.ok`) inherit the\n * gate for free.\n */\nexport function buildRegistry(dir: string): BuildResult {\n const model: LoadResult = load(dir);\n const diagnostics = validateSemantics(model);\n const hasErrors = diagnostics.some((d) => d.severity === \"error\");\n let ok = model.ok && !hasErrors;\n\n const registry = ok ? new Registry(compile(model), readHealthSnapshot(dir)) : undefined;\n if (registry) {\n for (const c of registry.toolNameCollisions) {\n ok = false;\n diagnostics.push({\n severity: \"error\",\n code: \"tool-name-collision\",\n message: `tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`,\n });\n }\n }\n\n return {\n ok,\n registry: ok ? registry : undefined,\n issues: model.issues,\n diagnostics,\n };\n}\n","// @archstone/runtime — MCP emitter (#7) — stdio entrypoint\n//\n// serveStdio builds the registry from disk (buildRegistry, registry.ts) and serves it over\n// stdio, the channel Claude Desktop uses. The fs-free MCP server construction\n// (toolDefinitions/callTool/createMcpServer) lives in ./server (ADD-0008 #27) — re-exported\n// here, alongside the semantic-lowering functions from @archstone/emitter-support, for\n// back-compat so nothing downstream breaks.\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport type { InvokeOptions } from \"@archstone/provider-rest\";\nimport { buildRegistry } from \"./registry\";\nimport { toolDefinitions, createMcpServer } from \"./server\";\n\nexport { toolName, inputJsonSchema, objectJsonSchema } from \"@archstone/emitter-support\";\n/** #44: the audit sink surface, re-exported so a deployer wiring `serveStdio`/`createMcpServer`\n * imports it from the package they already depend on. See `AuditSink`'s own doc comment for\n * the fire-and-forget contract and for the statement that the trail is best-effort and lossy. */\nexport { jsonLinesAuditSink, REDACTED, LIFECYCLE_BLOCKED_REASON } from \"@archstone/emitter-support\";\nexport type {\n AuditSink,\n AuditWritable,\n ExecutionRecord,\n ExecutionStatus,\n ExecutionPhase,\n ExecutionConsumer,\n ExecutionDenialReason,\n} from \"@archstone/emitter-support\";\nexport * from \"./server\";\n\n/**\n * Build the registry from a manifest dir and serve it over stdio (blocks).\n *\n * `invoke` (ADD-32) is forwarded verbatim to `createMcpServer` — this closes a real gap: prior\n * to #32, `serveStdio` passed NO `InvokeOptions` at all, so nothing (not `env`, not `caller`)\n * could ever be injected here. A stdio server is one child process per conversation (Claude\n * Desktop's model) — single-process, single-user by construction — so a static per-process\n * `invoke.caller` is architecturally sound here, unlike the HTTP case (`createHttpHandler`'s\n * `resolveCaller`, which must vary per inbound request).\n */\nexport async function serveStdio(dir: string, invoke?: InvokeOptions): Promise<void> {\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n // stdout is the MCP channel — all human output goes to stderr.\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n const tools = toolDefinitions(built.registry);\n console.error(`archstone: serving ${tools.length} tool(s) over stdio: ${tools.map((t) => t.name).join(\", \") || \"(none)\"}`);\n const server = createMcpServer(built.registry, invoke);\n await server.connect(new StdioServerTransport());\n}\n","// @archstone/runtime — Response mapper (ADD-12 / RFC-0006)\n//\n// Moved to @archstone/emitter-support (ADD-0008 #27), unchanged logic — re-exported here for\n// back-compat so nothing downstream breaks (verify.ts and existing consumers still import\n// from \"./mapping\").\nexport { applyResponseMapping, type MappingStatus, type MappingResult } from \"@archstone/emitter-support\";\n","// @archstone/runtime — Contract probe runner (ADD-18 / RFC-0006 Phase 2).\n//\n// `runVerify` replays a bound tool's golden fixture against the LIVE backend and\n// derives a health status. This is the only place outside a real MCP invocation that\n// makes a network call — always explicit, on demand (`archstone verify`), never\n// triggered by `apply`/`serve`. Reuses #12's `applyResponseMapping` verbatim (ADD-18\n// D-3/R-4): one mapper, so a probe VIOLATION is exactly what a real call would see.\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fingerprintShape, type IRTool, type IRResourceRegistry } from \"@archstone/compiler\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\nimport { evaluatePolicy } from \"@archstone/emitter-support\";\nimport { applyResponseMapping } from \"./mapping\";\n// ADD-24: HealthStatus's canonical home moved to @archstone/emitter-support (registry.ts's\n// exposure composition needs it, and runtime depends on emitter-support, never the reverse) —\n// re-exported here, unchanged, so nothing downstream (e.g. the CLI's `HealthStatus` import\n// from \"@archstone/runtime\") breaks.\nimport type { HealthStatus } from \"@archstone/emitter-support\";\nexport type { HealthStatus } from \"@archstone/emitter-support\";\n\nexport interface ToolVerification {\n capabilityId: string;\n status: HealthStatus;\n detail: string;\n /**\n * #43 (ADD-43 D-14): set iff this verification was refused by the policy evaluation point\n * before any request was issued — i.e. the probe observed **nothing at all** about the\n * backend's contract.\n *\n * Why an additive optional field rather than a fourth `HealthStatus` value: `HealthStatus` is\n * a CLOSED set already consumed by ADD-24's `combineExposure` and by ADD-20's published\n * `archstone verify --json` shape, so a `\"denied\"` member would take a published CLI contract\n * and the exposure severity ordering with it. `red` stays correct for the OPERATOR-facing\n * report — they asked \"is this binding healthy?\" and the honest answer is \"I could not\n * establish that\" (D-7).\n *\n * What this flag exists to stop is that `red` travelling ONWARD into an AGENT-facing surface.\n * `readHealthSnapshot` (registry.ts) skips any entry carrying it, so the tool ends up with no\n * health entry at all, `combineExposure` leaves its exposure untouched, and no hint is\n * appended to its advertised description. Without it, the documented ADD-24 D-8 workflow\n * (`archstone verify --json` > `.archstone-health.json`, then serve) would append\n * `\"binding health: red — the last contract verification failed\"` at the highest severity to\n * a policy-gated tool's description, for EVERY caller including permitted ones — a statement\n * that is factually false (no verification occurred) and that makes policy affect listing,\n * which BR-36 forbids. Because the CLI supplies no caller, that is the DEFAULT outcome for\n * any `allow`-bearing capability, not a corner case.\n *\n * The failure is silent: nothing throws, no exit code changes, an agent just reads a false\n * warning. `runtime/test/lifecycle.integration.test.ts` asserts it.\n */\n policyDenied?: true;\n}\n\nexport interface GoldenFixture {\n capabilityId: string;\n recordedAt?: string;\n request: Record<string, unknown>;\n expects?: { collectionNonEmpty?: boolean };\n}\n\nfunction readFixture(dir: string, path: string): GoldenFixture | undefined {\n try {\n return JSON.parse(readFileSync(resolve(dir, path), \"utf8\")) as GoldenFixture;\n } catch {\n return undefined;\n }\n}\n\n/** Verify one tool's contract against the live backend. Returns green/yellow/red — never\n * throws (a network/fs failure is itself a red result, not an exception the CLI must catch). */\nexport async function verifyTool(tool: IRTool, dir: string, resources: IRResourceRegistry, opts?: InvokeOptions): Promise<ToolVerification> {\n const base = { capabilityId: tool.id };\n const contract = tool.contract;\n if (!contract) return { ...base, status: \"red\", detail: \"no contract: declared — nothing to verify\" };\n\n const fixture = readFixture(dir, contract.probeFixture);\n if (!fixture) return { ...base, status: \"red\", detail: `fixture not found or unreadable: ${contract.probeFixture}` };\n\n // #43 (ADD-43 D-6): the contract prober is the THIRD invocation consumer, and it must route\n // through the same evaluation point as `callTool`/`executeCapability`. A probe makes a real\n // call with real credentials and is `authenticated`-gated today only because that gate lives\n // inside `invokeRest`; moving the gate (D-4) would silently un-gate `archstone verify` and the\n // published `runVerify()` unless this call exists. Placed immediately before `invokeRest`, so\n // \"no contract\" / \"fixture not found\" keep reporting themselves first.\n const decision = evaluatePolicy(tool, {\n principal: opts?.caller?.principal,\n credentialPresent: opts?.caller?.accessToken !== undefined,\n });\n if (!decision.allowed) {\n // `red`, with a detail textually distinguishable from the `live request failed:` prefix\n // below — because no live request was made (BR-37). `policyDenied` keeps this out of the\n // health snapshot entirely (D-14, see the field's doc comment).\n return {\n ...base,\n status: \"red\",\n detail: `policy denied before any request was made: ${decision.denial.message}`,\n policyDenied: true,\n };\n }\n\n const result = await invokeRest(tool, fixture.request, opts);\n if (!result.ok) return { ...base, status: \"red\", detail: `live request failed: ${result.error ?? `status ${result.status}`}` };\n\n const liveFingerprint = fingerprintShape(result.data);\n const fingerprintChanged = liveFingerprint !== contract.fingerprint;\n\n if (!tool.response) {\n // No response mapping to validate against — fingerprint drift is all we can see.\n return fingerprintChanged\n ? { ...base, status: \"yellow\", detail: `response shape changed (fingerprint ${contract.fingerprint} → ${liveFingerprint})` }\n : { ...base, status: \"green\", detail: \"fingerprint unchanged\" };\n }\n\n const mapped = applyResponseMapping(tool, result.data, resources);\n if (mapped.status === \"violation\") {\n return { ...base, status: \"red\", detail: `contract violation: missing required field(s) ${(mapped.missing ?? []).join(\", \")}` };\n }\n\n if (fixture.expects?.collectionNonEmpty) {\n const field = tool.response.field;\n const value = mapped.data?.[field];\n const empty = Array.isArray(value) ? value.length === 0 : value === undefined || value === null;\n if (empty) return { ...base, status: \"red\", detail: `expected a non-empty '${field}' collection; got none` };\n }\n\n if (mapped.status === \"degraded\") {\n return { ...base, status: \"yellow\", detail: `degraded: optional field(s) absent — ${(mapped.degraded ?? []).join(\", \")}` };\n }\n if (fingerprintChanged) {\n return { ...base, status: \"yellow\", detail: `response shape changed (fingerprint ${contract.fingerprint} → ${liveFingerprint}) but mapping still resolves` };\n }\n return { ...base, status: \"green\", detail: \"fingerprint unchanged, mapping OK\" };\n}\n\n/** Verify every contract-bearing tool in a registry. */\nexport async function runVerify(\n tools: IRTool[],\n dir: string,\n resources: IRResourceRegistry,\n opts?: InvokeOptions,\n): Promise<ToolVerification[]> {\n const contractBearing = tools.filter((t) => t.contract);\n return Promise.all(contractBearing.map((t) => verifyTool(t, dir, resources, opts)));\n}\n"],"mappings":"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","names":["Registry","readFileSync","readFileSync"]}
{
"name": "@archstone/runtime",
"version": "0.6.0",
"version": "0.7.0",
"private": false,

@@ -34,6 +34,6 @@ "type": "module",

"@modelcontextprotocol/sdk": "^1.12.0",
"@archstone/compiler": "0.6.0",
"@archstone/emitter-support": "0.6.0",
"@archstone/provider-rest": "0.6.0",
"@archstone/schema": "0.6.0"
"@archstone/compiler": "0.7.0",
"@archstone/emitter-support": "0.7.0",
"@archstone/provider-rest": "0.7.0",
"@archstone/schema": "0.7.0"
},

@@ -40,0 +40,0 @@ "devDependencies": {

// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { inputJsonSchema, objectJsonSchema, applyResponseMapping } from "@archstone/emitter-support";
import { invokeRest } from "@archstone/provider-rest";
function toolDefinitions(registry) {
const resources = registry.ir.resources;
return registry.invocableTools().filter(({ tool: t }) => registry.getExposure(t.id).listed).map(({ name, tool: t }) => {
const hint = registry.getExposure(t.id).hint;
const def = {
name,
description: hint ? `${t.description} (${hint.text})` : t.description,
inputSchema: inputJsonSchema(t.input, resources)
};
if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);
return def;
});
}
var CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
var LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
async function callTool(registry, name, args, opts) {
const tool = registry.getCapability(name);
if (!tool) {
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
}
const exposure = registry.getExposure(tool.id);
if (!exposure.invocable) {
return {
content: [{ type: "text", text: `capability '${tool.id}' is retired and can no longer be invoked.` }],
_meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: "lifecycle_blocked", capability: tool.id, lifecycle: tool.lifecycle } },
isError: true
};
}
const result = await invokeRest(tool, args, opts);
if (!result.ok) {
return { content: [{ type: "text", text: result.error ?? "invocation failed" }], isError: true };
}
if (tool.response) {
const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);
if (mapped.status === "violation") {
const missing = mapped.missing ?? [];
const text = `contract violation: capability '${tool.id}' \u2014 provider response is missing required field(s): ${missing.join(", ")}. Declared output shape not met; raw body withheld.`;
return {
content: [{ type: "text", text }],
_meta: { [CONTRACT_VIOLATION_META_KEY]: { error: "contract_violation", capability: tool.id, missing } },
isError: true
};
}
const content = [{ type: "text", text: JSON.stringify(mapped.data, null, 2) }];
if (mapped.status === "degraded") {
content.push({ type: "text", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(", ")}` });
}
return { content, structuredContent: mapped.data, isError: false };
}
const out = { content: [{ type: "text", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };
if (tool.output.length > 0) {
const data = result.data;
if (data && typeof data === "object" && !Array.isArray(data)) {
out.structuredContent = data;
}
}
return out;
}
function createMcpServer(registry, opts) {
const server = new Server({ name: "archstone", version: "0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const args = req.params.arguments ?? {};
const result = await callTool(registry, req.params.name, args, opts);
return result;
});
return server;
}
export {
toolDefinitions,
CONTRACT_VIOLATION_META_KEY,
LIFECYCLE_BLOCKED_META_KEY,
callTool,
createMcpServer
};
//# sourceMappingURL=chunk-C6DASWPB.js.map
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["// @archstone/runtime — MCP server construction (fs-free)\n//\n// Builds an MCP Server from a Registry and routes invocations through the REST provider\n// (#6). This is the ONLY place the MCP SDK appears (alongside stdio's transport wiring in\n// mcp.ts and the /http subpath's transport wiring in http.ts) — semantic-type → JSON-Schema\n// lowering itself now lives in @archstone/emitter-support (ADD-0008 #27), never here.\n//\n// Extracted out of mcp.ts (ADD-0008 #27) specifically so this module's graph never reaches\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) — only stdio's\n// `serveStdio` (mcp.ts) needs disk access. `http.ts` (the /http subpath) imports only this\n// file, so a consumer depending on that subpath alone stays fs-free.\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Registry, inputJsonSchema, objectJsonSchema, applyResponseMapping } from \"@archstone/emitter-support\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\n\ntype JsonSchema = Record<string, unknown>;\n\nexport interface McpToolDef {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n}\n\n/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's\n * `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function\n * (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two\n * consistent. Input and output fields lower against the IR resource registry, so a\n * `collection: Stay` output emits a typed, described `outputSchema` (not a bare\n * `{type:object}`).\n *\n * ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional\n * health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned\n * list entirely — unlisted, per D-10, though `experimental` remains callable by id (see\n * `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has\n * its text appended to `description` — the only MCP-specific rendering of the neutral\n * exposure the emitter-support layer computed. */\nexport function toolDefinitions(registry: Registry): McpToolDef[] {\n const resources = registry.ir.resources;\n return registry\n .invocableTools()\n .filter(({ tool: t }) => registry.getExposure(t.id).listed)\n .map(({ name, tool: t }) => {\n const hint = registry.getExposure(t.id).hint;\n const def: McpToolDef = {\n name,\n description: hint ? `${t.description} (${hint.text})` : t.description,\n inputSchema: inputJsonSchema(t.input, resources),\n };\n if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);\n return def;\n });\n}\n\nexport interface CallResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: Record<string, unknown>;\n isError: boolean;\n _meta?: Record<string, unknown>;\n}\n\n/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error\n * object is carried under. Never populated on `structuredContent` (D-3′) — the reference\n * MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool\n * declares one, regardless of `isError`, so a non-conforming error object there crashes the\n * client. `_meta` is untouched by that validation and passes through the client's zod parse\n * unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */\nexport const CONTRACT_VIOLATION_META_KEY = \"dev.archstone/contract_violation\";\n\n/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)\n * tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent\n * (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's\n * unconditional `structuredContent`-against-`outputSchema` validation never sees it. A\n * distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes \"this call was\n * blocked before any connector work\" from \"the provider's response violated the contract\". */\nexport const LIFECYCLE_BLOCKED_META_KEY = \"dev.archstone/lifecycle_blocked\";\n\n/** Route an MCP tool call to the REST provider and format the result as MCP content. */\nexport async function callTool(\n registry: Registry,\n name: string,\n args: Record<string, unknown>,\n opts?: InvokeOptions,\n): Promise<CallResult> {\n const tool = registry.getCapability(name);\n if (!tool) {\n return { content: [{ type: \"text\", text: `unknown tool: ${name}` }], isError: true };\n }\n\n // ADD-24 D-10/D-11: only `lifecycle: retired` sets `invocable:false` (health never does,\n // D-9) — checked immediately after resolution, before any connector/response work, same\n // call-site discipline the `contract_violation` check already uses downstream.\n const exposure = registry.getExposure(tool.id);\n if (!exposure.invocable) {\n return {\n content: [{ type: \"text\", text: `capability '${tool.id}' is retired and can no longer be invoked.` }],\n _meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: \"lifecycle_blocked\", capability: tool.id, lifecycle: tool.lifecycle } },\n isError: true,\n };\n }\n\n const result = await invokeRest(tool, args, opts);\n if (!result.ok) {\n return { content: [{ type: \"text\", text: result.error ?? \"invocation failed\" }], isError: true };\n }\n\n // #12 (ADD-12): a binding with a `response:` mapping is now MAPPED + VALIDATED against the\n // resource — the outputSchema (ADD-11) becomes an enforced contract, not just declared.\n if (tool.response) {\n const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);\n if (mapped.status === \"violation\") {\n // Fail closed (D-6): the declared output shape was not met — no raw pass-through.\n const missing = mapped.missing ?? [];\n const text = `contract violation: capability '${tool.id}' — provider response is missing required field(s): ${missing.join(\", \")}. Declared output shape not met; raw body withheld.`;\n // #19 (ADD-19 Rev 2 D-3′/D-6): structured error object lives in `_meta`, never\n // `structuredContent` — the reference SDK client validates `structuredContent` against\n // the tool's `outputSchema` unconditionally (not gated on `isError`), so a VIOLATION\n // object there (which never conforms to the success outputSchema) crashes the client\n // (verified live against the SDK's own InMemoryTransport, R2.0/R2.2). `capability` is\n // `tool.id`, the unsanitized CDL id — never the MCP-sanitized `name` lookup key (BR-7).\n return {\n content: [{ type: \"text\", text }],\n _meta: { [CONTRACT_VIOLATION_META_KEY]: { error: \"contract_violation\", capability: tool.id, missing } },\n isError: true,\n };\n }\n const content: CallResult[\"content\"] = [{ type: \"text\", text: JSON.stringify(mapped.data, null, 2) }];\n if (mapped.status === \"degraded\") {\n content.push({ type: \"text\", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(\", \")}` });\n }\n return { content, structuredContent: mapped.data, isError: false };\n }\n\n // No response mapping: today's raw pass-through (rollout-safe). The declared outputSchema is\n // NOT yet enforced for these tools — add a `response:` block to close the loop (ADD-12 R-3).\n const out: CallResult = { content: [{ type: \"text\", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };\n if (tool.output.length > 0) {\n const data = result.data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n out.structuredContent = data as Record<string, unknown>;\n }\n }\n return out;\n}\n\n/** Build an MCP Server that lists and invokes the registry's tools. */\nexport function createMcpServer(registry: Registry, opts?: InvokeOptions): Server {\n const server = new Server({ name: \"archstone\", version: \"0\" }, { capabilities: { tools: {} } });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const args = (req.params.arguments ?? {}) as Record<string, unknown>;\n const result = await callTool(registry, req.params.name, args, opts);\n return result as CallToolResult;\n });\n\n return server;\n}\n"],"mappings":"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","names":[]}
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions } from '@archstone/provider-rest';
type JsonSchema = Record<string, unknown>;
interface McpToolDef {
name: string;
description: string;
inputSchema: JsonSchema;
outputSchema?: JsonSchema;
}
/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's
* `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function
* (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two
* consistent. Input and output fields lower against the IR resource registry, so a
* `collection: Stay` output emits a typed, described `outputSchema` (not a bare
* `{type:object}`).
*
* ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional
* health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned
* list entirely — unlisted, per D-10, though `experimental` remains callable by id (see
* `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has
* its text appended to `description` — the only MCP-specific rendering of the neutral
* exposure the emitter-support layer computed. */
declare function toolDefinitions(registry: Registry): McpToolDef[];
interface CallResult {
content: {
type: "text";
text: string;
}[];
structuredContent?: Record<string, unknown>;
isError: boolean;
_meta?: Record<string, unknown>;
}
/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error
* object is carried under. Never populated on `structuredContent` (D-3′) — the reference
* MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool
* declares one, regardless of `isError`, so a non-conforming error object there crashes the
* client. `_meta` is untouched by that validation and passes through the client's zod parse
* unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */
declare const CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)
* tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent
* (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's
* unconditional `structuredContent`-against-`outputSchema` validation never sees it. A
* distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes "this call was
* blocked before any connector work" from "the provider's response violated the contract". */
declare const LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
/** Route an MCP tool call to the REST provider and format the result as MCP content. */
declare function callTool(registry: Registry, name: string, args: Record<string, unknown>, opts?: InvokeOptions): Promise<CallResult>;
/** Build an MCP Server that lists and invokes the registry's tools. */
declare function createMcpServer(registry: Registry, opts?: InvokeOptions): Server;
export { CONTRACT_VIOLATION_META_KEY as C, LIFECYCLE_BLOCKED_META_KEY as L, type McpToolDef as M, type CallResult as a, callTool as b, createMcpServer as c, toolDefinitions as t };