@atrib/daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare const MISSING_CONTEXT_ERROR_TEXT = "atrib: context_id required on stateless transport";
3
+ export type HttpContextPolicyOutcome = {
4
+ kind: 'pass';
5
+ params: CallToolRequest['params'];
6
+ } | {
7
+ kind: 'injected';
8
+ params: CallToolRequest['params'];
9
+ } | {
10
+ kind: 'rejected';
11
+ result: CallToolResult;
12
+ };
13
+ /**
14
+ * Apply the stateless-HTTP context policy to one tools/call request.
15
+ * Never mutates the caller's params object.
16
+ */
17
+ export declare function applyHttpContextPolicy(params: CallToolRequest['params'], options: {
18
+ ambientContext: boolean;
19
+ }): HttpContextPolicyOutcome;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Context-identity policy for the stateless HTTP surface (P046).
4
+ *
5
+ * Precedence per request, citing the single canonical inbound-carrier
6
+ * ladder (spec §1.5.4 with the §1.5.3 `X-Atrib-Chain` fallback, as
7
+ * `readInboundContext` in @atrib/mcp implements):
8
+ *
9
+ * 1. Explicit `context_id` tool argument (32-hex). Passed through
10
+ * untouched; the daemon adds nothing.
11
+ * 2. Inbound carrier resolution from per-request `_meta`. When the
12
+ * carriers resolve a context_id (traceparent trace-id) the daemon
13
+ * injects it as the explicit argument; when they also resolve a
14
+ * propagation token, the token's record hash is injected as
15
+ * `chain_root` on tools whose schema accepts it, seeding chain-tail
16
+ * resolution per D067 rung 1.
17
+ * 3. Nothing resolves: write primitives get a typed tool error
18
+ * (`atrib: context_id required on stateless transport`). Read
19
+ * primitives that support unscoped queries proceed per their own
20
+ * scope rules. A single-tenant daemon may opt back into ambient
21
+ * env/profile-file discovery (D083 v3) with the ambient-context
22
+ * flag, in which case the call passes through and the primitive's
23
+ * own D078/D083 ladder applies.
24
+ *
25
+ * Chain-root selection itself is never reimplemented here: the injected
26
+ * values feed `resolveChainRoot` through the primitives' existing
27
+ * caller-argument path, and its D067 precedence is untouched.
28
+ */
29
+ import { hexEncode, readInboundContext } from '@atrib/mcp';
30
+ import { WRITE_TOOL_NAMES } from './backend.js';
31
+ const CONTEXT_ID_PATTERN = /^[0-9a-f]{32}$/;
32
+ /** Write tools whose input schema accepts a caller-managed chain_root. */
33
+ const CHAIN_ROOT_CAPABLE_TOOLS = new Set(['emit']);
34
+ export const MISSING_CONTEXT_ERROR_TEXT = 'atrib: context_id required on stateless transport';
35
+ function argumentsRecord(params) {
36
+ const args = params.arguments;
37
+ if (args && typeof args === 'object' && !Array.isArray(args)) {
38
+ return args;
39
+ }
40
+ return {};
41
+ }
42
+ /**
43
+ * Apply the stateless-HTTP context policy to one tools/call request.
44
+ * Never mutates the caller's params object.
45
+ */
46
+ export function applyHttpContextPolicy(params, options) {
47
+ if (!WRITE_TOOL_NAMES.has(params.name)) {
48
+ return { kind: 'pass', params };
49
+ }
50
+ const args = argumentsRecord(params);
51
+ const explicit = args['context_id'];
52
+ if (typeof explicit === 'string' && CONTEXT_ID_PATTERN.test(explicit)) {
53
+ return { kind: 'pass', params };
54
+ }
55
+ // Rung 2: per-request _meta carriers (§1.5.4 ladder + §1.5.3 fallback).
56
+ const inbound = readInboundContext(params);
57
+ if (inbound?.contextId && CONTEXT_ID_PATTERN.test(inbound.contextId)) {
58
+ const injected = { ...args, context_id: inbound.contextId };
59
+ if (CHAIN_ROOT_CAPABLE_TOOLS.has(params.name) &&
60
+ args['chain_root'] === undefined &&
61
+ inbound.recordHash.length === 32) {
62
+ injected['chain_root'] = `sha256:${hexEncode(inbound.recordHash)}`;
63
+ }
64
+ return { kind: 'injected', params: { ...params, arguments: injected } };
65
+ }
66
+ if (options.ambientContext) {
67
+ return { kind: 'pass', params };
68
+ }
69
+ return {
70
+ kind: 'rejected',
71
+ result: {
72
+ content: [{ type: 'text', text: MISSING_CONTEXT_ERROR_TEXT }],
73
+ isError: true,
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Stateless Streamable HTTP host for atribd.
3
+ *
4
+ * Every request is self-describing and any request can land on any
5
+ * instance. There is no session table, no idle sweeper, and no
6
+ * initialize-first gate; the session machinery the 2026-07-28 MCP spec
7
+ * removes is deleted rather than emulated. What replaces it:
8
+ *
9
+ * - Routing-header validation (SEP-2243): when `Mcp-Method` / `Mcp-Name`
10
+ * headers are present they must match the parsed JSON-RPC body; a
11
+ * mismatch is HTTP 400 with a JSON-RPC error and no state consulted.
12
+ * Absent headers are accepted during the legacy compatibility window.
13
+ * - Per-request `_meta` (SEP-414): inbound carriers resolve per request
14
+ * through the §1.5.4/§1.5.3 ladder inside the tools/call handler.
15
+ * - Cache metadata (SEP-2549): tools/list responses carry `ttlMs` and
16
+ * `cacheScope` so clients can cache the tool catalogue.
17
+ * - Legacy compatibility window: a pre-2026-07-28 client that POSTs
18
+ * `initialize` gets a valid stateless response (capabilities returned,
19
+ * no session id issued); requests carrying `Mcp-Session-Id` are served
20
+ * with the header ignored, never 404.
21
+ *
22
+ * Per §5.8 the daemon never blocks a primary tool call on network state;
23
+ * a write-primitive call that cannot resolve a context returns a typed
24
+ * tool error to its own caller (the primitive call IS the primary call
25
+ * on this surface).
26
+ */
27
+ import { type Server as HttpServer } from 'node:http';
28
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
29
+ import { type AtribdBackend } from './backend.js';
30
+ import { type AtribdTransportAdapter } from './transport-adapter.js';
31
+ export declare const DEFAULT_HTTP_HOST = "127.0.0.1";
32
+ export declare const DEFAULT_HTTP_PORT = 8796;
33
+ export declare const DEFAULT_HTTP_PATH = "/mcp";
34
+ export declare const DEFAULT_TOOLS_LIST_TTL_MS: number;
35
+ export interface AtribdRequestCounters {
36
+ served: number;
37
+ rejected_header_mismatch: number;
38
+ rejected_missing_context: number;
39
+ legacy_initialize: number;
40
+ legacy_session_header_ignored: number;
41
+ method_not_allowed: number;
42
+ }
43
+ export interface AtribdHttpHost {
44
+ endpoint: string;
45
+ healthEndpoint: string;
46
+ server: HttpServer;
47
+ requestCounters(): AtribdRequestCounters;
48
+ close(): Promise<void>;
49
+ }
50
+ export interface AtribdHttpHostOptions {
51
+ host?: string;
52
+ port?: number;
53
+ path?: string;
54
+ jsonReady?: boolean;
55
+ toolTimeoutMs?: number;
56
+ /** SEP-2549 freshness hint advertised on tools/list responses. */
57
+ toolsListTtlMs?: number;
58
+ /**
59
+ * Opt a single-tenant daemon back into ambient context discovery
60
+ * (D078/D083 env/profile-file ladder) on HTTP, where explicit-required
61
+ * is the default. Working flag name; final name is a P046 open question.
62
+ */
63
+ ambientContext?: boolean;
64
+ backendFactory?: () => Promise<AtribdBackend>;
65
+ adapterFactory?: (serverFactory: () => Server) => AtribdTransportAdapter;
66
+ }
67
+ export declare function normalizeMcpPath(raw: string): string;
68
+ export declare function healthPathFor(mcpPath: string): string;
69
+ /**
70
+ * SEP-2243: when routing headers are present they MUST match the body.
71
+ * Returns an error string on mismatch, undefined when consistent (or when
72
+ * the headers are absent, which the legacy window tolerates).
73
+ */
74
+ export declare function routingHeaderMismatch(mcpMethod: string | undefined, mcpName: string | undefined, body: unknown): string | undefined;
75
+ export declare function httpEndpoint(host: string, port: number, path: string): string;
76
+ export interface AtribdServerFactoryOptions {
77
+ getBackend: () => Promise<AtribdBackend>;
78
+ toolsListTtlMs: number;
79
+ /** Applied to write-primitive tools/call requests on the HTTP surface. */
80
+ httpContextPolicy?: {
81
+ ambientContext: boolean;
82
+ onInjected?: () => void;
83
+ onRejected?: () => void;
84
+ };
85
+ }
86
+ /**
87
+ * Outer MCP server wired to the shared backend. The HTTP host applies the
88
+ * stateless context policy inside the tools/call handler; the stdio
89
+ * runtime omits it so the D078/D083 ambient ladder keeps working for
90
+ * startup-spawn harnesses.
91
+ */
92
+ export declare function createAtribdServer(options: AtribdServerFactoryOptions): Server;
93
+ export declare function bindAtribdHttpHost(options?: AtribdHttpHostOptions): Promise<AtribdHttpHost>;
@@ -0,0 +1,476 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Stateless Streamable HTTP host for atribd.
4
+ *
5
+ * Every request is self-describing and any request can land on any
6
+ * instance. There is no session table, no idle sweeper, and no
7
+ * initialize-first gate; the session machinery the 2026-07-28 MCP spec
8
+ * removes is deleted rather than emulated. What replaces it:
9
+ *
10
+ * - Routing-header validation (SEP-2243): when `Mcp-Method` / `Mcp-Name`
11
+ * headers are present they must match the parsed JSON-RPC body; a
12
+ * mismatch is HTTP 400 with a JSON-RPC error and no state consulted.
13
+ * Absent headers are accepted during the legacy compatibility window.
14
+ * - Per-request `_meta` (SEP-414): inbound carriers resolve per request
15
+ * through the §1.5.4/§1.5.3 ladder inside the tools/call handler.
16
+ * - Cache metadata (SEP-2549): tools/list responses carry `ttlMs` and
17
+ * `cacheScope` so clients can cache the tool catalogue.
18
+ * - Legacy compatibility window: a pre-2026-07-28 client that POSTs
19
+ * `initialize` gets a valid stateless response (capabilities returned,
20
+ * no session id issued); requests carrying `Mcp-Session-Id` are served
21
+ * with the header ignored, never 404.
22
+ *
23
+ * Per §5.8 the daemon never blocks a primary tool call on network state;
24
+ * a write-primitive call that cannot resolve a context returns a typed
25
+ * tool error to its own caller (the primitive call IS the primary call
26
+ * on this surface).
27
+ */
28
+ import { createServer, } from 'node:http';
29
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
30
+ import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest, } from '@modelcontextprotocol/sdk/types.js';
31
+ import { createAtribdBackend, errorMessage, logDaemonEvent, readPackageVersion, runtimeContractsDegraded, toolCallDiagnosticsDegraded, DEFAULT_TOOL_TIMEOUT_MS, } from './backend.js';
32
+ import { applyHttpContextPolicy } from './context-policy.js';
33
+ import { createSessionSdkStatelessAdapter, } from './transport-adapter.js';
34
+ export const DEFAULT_HTTP_HOST = '127.0.0.1';
35
+ export const DEFAULT_HTTP_PORT = 8796;
36
+ export const DEFAULT_HTTP_PATH = '/mcp';
37
+ export const DEFAULT_TOOLS_LIST_TTL_MS = 24 * 60 * 60 * 1000;
38
+ const MAX_JSON_BODY_BYTES = 1024 * 1024;
39
+ function createBackendProvider(factory) {
40
+ const startedAt = Date.now();
41
+ let status = { state: 'starting', startedAt };
42
+ const ready = factory().then((backend) => {
43
+ status = { state: 'ready', startedAt, readyAt: Date.now(), backend };
44
+ return backend;
45
+ }, (error) => {
46
+ status = { state: 'error', startedAt, errorAt: Date.now(), error };
47
+ throw error;
48
+ });
49
+ void ready.catch(() => { });
50
+ return {
51
+ get: () => ready,
52
+ status: () => status,
53
+ close: async () => {
54
+ const backend = status.state === 'ready' ? status.backend : await ready.catch(() => undefined);
55
+ await backend?.close();
56
+ },
57
+ };
58
+ }
59
+ export function normalizeMcpPath(raw) {
60
+ const withSlash = raw.startsWith('/') ? raw : `/${raw}`;
61
+ let end = withSlash.length;
62
+ while (end > 1 && withSlash.charCodeAt(end - 1) === 47)
63
+ end -= 1;
64
+ const trimmed = withSlash.slice(0, end);
65
+ return trimmed.length > 0 ? trimmed : DEFAULT_HTTP_PATH;
66
+ }
67
+ export function healthPathFor(mcpPath) {
68
+ return mcpPath === '/' ? '/health' : `${mcpPath}/health`;
69
+ }
70
+ function requestPath(req) {
71
+ try {
72
+ return new URL(req.url ?? '/', 'http://localhost').pathname;
73
+ }
74
+ catch {
75
+ return '/';
76
+ }
77
+ }
78
+ function sendJson(res, status, body) {
79
+ const bytes = Buffer.from(JSON.stringify(body));
80
+ res.statusCode = status;
81
+ res.setHeader('content-type', 'application/json; charset=utf-8');
82
+ res.setHeader('content-length', bytes.length);
83
+ res.end(bytes);
84
+ }
85
+ function sendJsonRpcError(res, status, code, message) {
86
+ sendJson(res, status, {
87
+ jsonrpc: '2.0',
88
+ error: { code, message },
89
+ id: null,
90
+ });
91
+ }
92
+ async function readJsonBody(req) {
93
+ const chunks = [];
94
+ let total = 0;
95
+ for await (const chunk of req) {
96
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
97
+ total += buf.length;
98
+ if (total > MAX_JSON_BODY_BYTES) {
99
+ throw new Error(`request body exceeds ${MAX_JSON_BODY_BYTES} bytes`);
100
+ }
101
+ chunks.push(buf);
102
+ }
103
+ if (chunks.length === 0)
104
+ return undefined;
105
+ const raw = Buffer.concat(chunks).toString('utf8');
106
+ if (raw.trim().length === 0)
107
+ return undefined;
108
+ return JSON.parse(raw);
109
+ }
110
+ function headerValue(req, name) {
111
+ const raw = req.headers[name];
112
+ if (Array.isArray(raw))
113
+ return raw[0];
114
+ return raw;
115
+ }
116
+ /**
117
+ * Collect JSON-RPC method names (and tools/call tool names) from a parsed
118
+ * body. Accepts a single message or a legacy batch array; malformed
119
+ * entries contribute nothing and are left for the adapter to reject.
120
+ */
121
+ function describeBody(body) {
122
+ const messages = Array.isArray(body) ? body : [body];
123
+ const methods = [];
124
+ const toolNames = [];
125
+ for (const message of messages) {
126
+ if (!message || typeof message !== 'object')
127
+ continue;
128
+ const method = message.method;
129
+ if (typeof method !== 'string')
130
+ continue;
131
+ methods.push(method);
132
+ if (method === 'tools/call') {
133
+ const params = message.params;
134
+ if (params && typeof params === 'object' && !Array.isArray(params)) {
135
+ const name = params.name;
136
+ if (typeof name === 'string')
137
+ toolNames.push(name);
138
+ }
139
+ }
140
+ }
141
+ return { methods, toolNames };
142
+ }
143
+ /**
144
+ * SEP-2243: when routing headers are present they MUST match the body.
145
+ * Returns an error string on mismatch, undefined when consistent (or when
146
+ * the headers are absent, which the legacy window tolerates).
147
+ */
148
+ export function routingHeaderMismatch(mcpMethod, mcpName, body) {
149
+ if (mcpMethod === undefined && mcpName === undefined)
150
+ return undefined;
151
+ const { methods, toolNames } = describeBody(body);
152
+ if (mcpMethod !== undefined) {
153
+ if (methods.length === 0) {
154
+ return `Mcp-Method header ${mcpMethod} does not match a body with no request method`;
155
+ }
156
+ const mismatch = methods.find((method) => method !== mcpMethod);
157
+ if (mismatch !== undefined) {
158
+ return `Mcp-Method header ${mcpMethod} does not match body method ${mismatch}`;
159
+ }
160
+ }
161
+ if (mcpName !== undefined) {
162
+ if (!methods.includes('tools/call')) {
163
+ return `Mcp-Name header ${mcpName} does not match a body with no tools/call request`;
164
+ }
165
+ const mismatch = toolNames.find((name) => name !== mcpName);
166
+ if (mismatch !== undefined) {
167
+ return `Mcp-Name header ${mcpName} does not match body tool name ${mismatch}`;
168
+ }
169
+ if (toolNames.length === 0) {
170
+ return `Mcp-Name header ${mcpName} does not match a tools/call body with no tool name`;
171
+ }
172
+ }
173
+ return undefined;
174
+ }
175
+ function listen(server, host, port) {
176
+ return new Promise((resolve, reject) => {
177
+ const onError = (error) => {
178
+ server.off('listening', onListening);
179
+ reject(error);
180
+ };
181
+ const onListening = () => {
182
+ server.off('error', onError);
183
+ resolve();
184
+ };
185
+ server.once('error', onError);
186
+ server.once('listening', onListening);
187
+ server.listen(port, host);
188
+ });
189
+ }
190
+ function closeHttpServer(server) {
191
+ return new Promise((resolve, reject) => {
192
+ server.close((error) => {
193
+ if (error)
194
+ reject(error);
195
+ else
196
+ resolve();
197
+ });
198
+ });
199
+ }
200
+ export function httpEndpoint(host, port, path) {
201
+ const literalHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
202
+ return `http://${literalHost}:${port}${path}`;
203
+ }
204
+ function actualPort(server) {
205
+ const address = server.address();
206
+ if (!address || typeof address === 'string')
207
+ return DEFAULT_HTTP_PORT;
208
+ return address.port;
209
+ }
210
+ /**
211
+ * Outer MCP server wired to the shared backend. The HTTP host applies the
212
+ * stateless context policy inside the tools/call handler; the stdio
213
+ * runtime omits it so the D078/D083 ambient ladder keeps working for
214
+ * startup-spawn harnesses.
215
+ */
216
+ export function createAtribdServer(options) {
217
+ const server = new Server({
218
+ name: 'atribd',
219
+ version: readPackageVersion(),
220
+ }, {
221
+ capabilities: { tools: {} },
222
+ instructions: 'atribd: one local daemon exposing all seven atrib cognitive primitives. ' +
223
+ 'Pass context_id explicitly on every write-primitive call over HTTP.',
224
+ });
225
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
226
+ const backend = await options.getBackend();
227
+ // SEP-2549 cache metadata: ttlMs is a freshness hint in milliseconds;
228
+ // cacheScope 'private' keeps shared intermediaries from caching a
229
+ // per-host tool catalogue.
230
+ return {
231
+ tools: backend.tools,
232
+ ttlMs: options.toolsListTtlMs,
233
+ cacheScope: 'private',
234
+ };
235
+ });
236
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
237
+ const backend = await options.getBackend();
238
+ const policy = options.httpContextPolicy;
239
+ if (!policy) {
240
+ return backend.callTool(request.params);
241
+ }
242
+ const outcome = applyHttpContextPolicy(request.params, {
243
+ ambientContext: policy.ambientContext,
244
+ });
245
+ if (outcome.kind === 'rejected') {
246
+ policy.onRejected?.();
247
+ logDaemonEvent({
248
+ event: 'write_call_rejected_missing_context',
249
+ tool: request.params.name,
250
+ });
251
+ return outcome.result;
252
+ }
253
+ if (outcome.kind === 'injected') {
254
+ policy.onInjected?.();
255
+ }
256
+ return backend.callTool(outcome.params);
257
+ });
258
+ return server;
259
+ }
260
+ export async function bindAtribdHttpHost(options = {}) {
261
+ const toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
262
+ const toolsListTtlMs = options.toolsListTtlMs ?? DEFAULT_TOOLS_LIST_TTL_MS;
263
+ const ambientContext = options.ambientContext ?? false;
264
+ const backendProvider = createBackendProvider(options.backendFactory ?? (() => createAtribdBackend({ toolTimeoutMs })));
265
+ const host = options.host ?? DEFAULT_HTTP_HOST;
266
+ const port = options.port ?? DEFAULT_HTTP_PORT;
267
+ const mcpPath = normalizeMcpPath(options.path ?? DEFAULT_HTTP_PATH);
268
+ const healthPath = healthPathFor(mcpPath);
269
+ const sockets = new Set();
270
+ const version = readPackageVersion();
271
+ const counters = {
272
+ served: 0,
273
+ rejected_header_mismatch: 0,
274
+ rejected_missing_context: 0,
275
+ legacy_initialize: 0,
276
+ legacy_session_header_ignored: 0,
277
+ method_not_allowed: 0,
278
+ };
279
+ let endpoint = '';
280
+ let healthEndpoint = '';
281
+ const serverFactory = () => createAtribdServer({
282
+ getBackend: backendProvider.get,
283
+ toolsListTtlMs,
284
+ httpContextPolicy: {
285
+ ambientContext,
286
+ onRejected: () => {
287
+ counters.rejected_missing_context += 1;
288
+ },
289
+ },
290
+ });
291
+ const adapterFactory = options.adapterFactory ??
292
+ ((factory) => createSessionSdkStatelessAdapter({ serverFactory: factory }));
293
+ const adapter = adapterFactory(serverFactory);
294
+ const server = createServer(async (req, res) => {
295
+ const path = requestPath(req);
296
+ if (path === healthPath || path === '/health') {
297
+ const backendStatus = backendProvider.status();
298
+ const daemon = {
299
+ name: 'atribd',
300
+ version,
301
+ pid: process.pid,
302
+ transport: 'streamable-http-stateless',
303
+ transport_adapter: adapter.name,
304
+ protocol_version: adapter.protocolVersion,
305
+ endpoint,
306
+ health_endpoint: healthEndpoint,
307
+ };
308
+ if (backendStatus.state === 'starting') {
309
+ sendJson(res, 503, {
310
+ status: 'starting',
311
+ report: {
312
+ daemon: {
313
+ ...daemon,
314
+ backend: 'starting',
315
+ tool_count: 0,
316
+ mounted_primitive_count: 0,
317
+ backend_started_at: backendStatus.startedAt,
318
+ },
319
+ requests: { ...counters },
320
+ },
321
+ });
322
+ return;
323
+ }
324
+ if (backendStatus.state === 'error') {
325
+ sendJson(res, 500, {
326
+ status: 'error',
327
+ report: {
328
+ daemon: {
329
+ ...daemon,
330
+ backend: 'error',
331
+ error: errorMessage(backendStatus.error),
332
+ backend_started_at: backendStatus.startedAt,
333
+ backend_error_at: backendStatus.errorAt,
334
+ },
335
+ requests: { ...counters },
336
+ },
337
+ });
338
+ return;
339
+ }
340
+ const backend = backendStatus.backend;
341
+ const toolCalls = backend.diagnostics();
342
+ const runtimeContracts = backend.runtimeContracts();
343
+ const status = toolCallDiagnosticsDegraded(toolCalls) || runtimeContractsDegraded(runtimeContracts)
344
+ ? 'degraded'
345
+ : 'healthy';
346
+ let activeHttpConnections = 0;
347
+ for (const socket of sockets) {
348
+ if (!socket.destroyed && socket !== req.socket)
349
+ activeHttpConnections += 1;
350
+ }
351
+ sendJson(res, 200, {
352
+ status,
353
+ report: {
354
+ daemon: {
355
+ ...daemon,
356
+ backend: 'shared',
357
+ tool_count: backend.toolNames.length,
358
+ mounted_primitive_count: backend.mountedPrimitiveCount,
359
+ active_http_connections: activeHttpConnections,
360
+ tools_list_ttl_ms: toolsListTtlMs,
361
+ },
362
+ primitive_contracts: runtimeContracts.primitives,
363
+ behavioral_probes: runtimeContracts.behavioral_probes,
364
+ recall_contract: runtimeContracts.recall_content,
365
+ profile: {
366
+ agent: process.env.ATRIB_AGENT,
367
+ mirror_file: process.env.ATRIB_MIRROR_FILE,
368
+ local_substrate_endpoint: process.env.ATRIB_LOCAL_SUBSTRATE_ENDPOINT,
369
+ context_id_policy: ambientContext ? 'ambient-opt-in' : 'explicit-required',
370
+ requires_explicit_context_id: !ambientContext,
371
+ },
372
+ requests: { ...counters },
373
+ tool_calls: toolCalls,
374
+ },
375
+ });
376
+ return;
377
+ }
378
+ if (path !== mcpPath) {
379
+ sendJsonRpcError(res, 404, -32000, 'Not Found');
380
+ return;
381
+ }
382
+ // Stateless surface: POST only. The Streamable HTTP spec permits 405
383
+ // for servers that offer no server-initiated streams (GET) and no
384
+ // session termination (DELETE).
385
+ if (req.method !== 'POST') {
386
+ counters.method_not_allowed += 1;
387
+ res.setHeader('allow', 'POST');
388
+ sendJsonRpcError(res, 405, -32000, 'Method Not Allowed');
389
+ return;
390
+ }
391
+ try {
392
+ let body;
393
+ try {
394
+ body = await readJsonBody(req);
395
+ }
396
+ catch (error) {
397
+ sendJsonRpcError(res, 400, -32700, `invalid JSON body: ${errorMessage(error)}`);
398
+ return;
399
+ }
400
+ // SEP-2243 routing-header validation: headers, when present, must
401
+ // match the body. No state is consulted on the mismatch path.
402
+ const mismatch = routingHeaderMismatch(headerValue(req, 'mcp-method'), headerValue(req, 'mcp-name'), body);
403
+ if (mismatch) {
404
+ counters.rejected_header_mismatch += 1;
405
+ sendJsonRpcError(res, 400, -32600, `routing header mismatch: ${mismatch}`);
406
+ return;
407
+ }
408
+ // Legacy compatibility window: session headers are ignored, never
409
+ // validated; a legacy initialize is answered without session issuance.
410
+ if (headerValue(req, 'mcp-session-id') !== undefined) {
411
+ counters.legacy_session_header_ignored += 1;
412
+ }
413
+ if (isInitializeRequest(body)) {
414
+ counters.legacy_initialize += 1;
415
+ }
416
+ counters.served += 1;
417
+ await adapter.handleRequest(req, res, body);
418
+ }
419
+ catch (error) {
420
+ if (!res.headersSent) {
421
+ sendJsonRpcError(res, 500, -32603, `Internal server error: ${errorMessage(error)}`);
422
+ }
423
+ }
424
+ });
425
+ server.on('connection', (socket) => {
426
+ sockets.add(socket);
427
+ socket.on('close', () => {
428
+ sockets.delete(socket);
429
+ });
430
+ });
431
+ try {
432
+ await listen(server, host, port);
433
+ }
434
+ catch (error) {
435
+ await backendProvider.close().catch(() => { });
436
+ throw error;
437
+ }
438
+ const boundPort = actualPort(server);
439
+ endpoint = httpEndpoint(host, boundPort, mcpPath);
440
+ healthEndpoint = httpEndpoint(host, boundPort, healthPath);
441
+ if (options.jsonReady) {
442
+ void backendProvider
443
+ .get()
444
+ .then((backend) => {
445
+ process.stdout.write(`${JSON.stringify({
446
+ status: 'ready',
447
+ name: 'atribd',
448
+ version,
449
+ pid: process.pid,
450
+ transport: 'streamable-http-stateless',
451
+ transport_adapter: adapter.name,
452
+ endpoint,
453
+ health_endpoint: healthEndpoint,
454
+ tool_count: backend.toolNames.length,
455
+ mounted_primitive_count: backend.mountedPrimitiveCount,
456
+ })}\n`);
457
+ })
458
+ .catch((error) => {
459
+ process.stderr.write(`atribd: backend failed: ${errorMessage(error)}\n`);
460
+ });
461
+ }
462
+ return {
463
+ endpoint,
464
+ healthEndpoint,
465
+ server,
466
+ requestCounters: () => ({ ...counters }),
467
+ close: async () => {
468
+ try {
469
+ await closeHttpServer(server);
470
+ }
471
+ finally {
472
+ await backendProvider.close();
473
+ }
474
+ },
475
+ };
476
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ export { createAtribdBackend, callWithToolTimeout, readPackageVersion, runtimeContractsDegraded, toolCallDiagnosticsDegraded, writeSerializationKey, ContextWriteLocks, logDaemonEvent, errorMessage, PRIMITIVE_SPECS, WRITE_TOOL_NAMES, DEFAULT_TOOL_TIMEOUT_MS, type AtribdBackend, type AtribdBackendOptions, type AtribdPrimitiveFactory, type AtribdPrimitiveHandle, type AtribdHandlerKind, type AtribdDiagnostics, type AtribdToolCallDiagnostic, type AtribdRuntimeContracts, type AtribdRuntimeContractDiagnostic, type AtribdSurfaceContractDiagnostic, type AtribdBehavioralProbeDiagnostic, } from './backend.js';
3
+ export { applyHttpContextPolicy, MISSING_CONTEXT_ERROR_TEXT, type HttpContextPolicyOutcome, } from './context-policy.js';
4
+ export { createSessionSdkStatelessAdapter, type AtribdTransportAdapter, type SessionSdkStatelessAdapterOptions, } from './transport-adapter.js';
5
+ export { bindAtribdHttpHost, createAtribdServer, routingHeaderMismatch, httpEndpoint, normalizeMcpPath, healthPathFor, DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, DEFAULT_HTTP_PATH, DEFAULT_TOOLS_LIST_TTL_MS, type AtribdHttpHost, type AtribdHttpHostOptions, type AtribdRequestCounters, type AtribdServerFactoryOptions, } from './http-host.js';
6
+ export { createAtribdRuntime, createAtribdHttpProxyRuntime, type AtribdRuntime, type AtribdRuntimeOptions, } from './stdio.js';
7
+ type TransportMode = 'stdio' | 'streamable-http' | 'stdio-http-proxy';
8
+ interface CliOptions {
9
+ transport: TransportMode;
10
+ host: string;
11
+ port: number;
12
+ path: string;
13
+ endpoint: string;
14
+ json: boolean;
15
+ toolTimeoutMs: number;
16
+ toolsListTtlMs: number;
17
+ ambientContext: boolean;
18
+ help: boolean;
19
+ }
20
+ export declare function parseCliOptions(argv: readonly string[]): CliOptions;