@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.
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * atribd: the local daemon for atrib's seven cognitive primitives.
5
+ *
6
+ * One host-owned process mounts the seven primitive MCP servers in process
7
+ * and serves their fifteen physical tools as thin aliases over two internal
8
+ * handlers (write, read). Transports: stateless Streamable HTTP (the
9
+ * recommended daemon topology), direct stdio (in-process), and a
10
+ * stdio-to-HTTP proxy shim for startup-spawn harnesses.
11
+ *
12
+ * Signed records are byte-identical to the standalone per-primitive
13
+ * binaries: the daemon calls the same handler code paths with the same
14
+ * `_local.producer` sidecar labels, and never reimplements chain-root
15
+ * selection (D067; `resolveChainRoot` in @atrib/mcp stays the single
16
+ * source of truth).
17
+ */
18
+ import { pathToFileURL } from 'node:url';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { bindAtribdHttpHost, httpEndpoint, normalizeMcpPath, DEFAULT_HTTP_HOST, DEFAULT_HTTP_PATH, DEFAULT_HTTP_PORT, DEFAULT_TOOLS_LIST_TTL_MS, } from './http-host.js';
21
+ import { createAtribdHttpProxyRuntime, createAtribdRuntime } from './stdio.js';
22
+ import { DEFAULT_TOOL_TIMEOUT_MS } from './backend.js';
23
+ export { createAtribdBackend, callWithToolTimeout, readPackageVersion, runtimeContractsDegraded, toolCallDiagnosticsDegraded, writeSerializationKey, ContextWriteLocks, logDaemonEvent, errorMessage, PRIMITIVE_SPECS, WRITE_TOOL_NAMES, DEFAULT_TOOL_TIMEOUT_MS, } from './backend.js';
24
+ export { applyHttpContextPolicy, MISSING_CONTEXT_ERROR_TEXT, } from './context-policy.js';
25
+ export { createSessionSdkStatelessAdapter, } from './transport-adapter.js';
26
+ export { bindAtribdHttpHost, createAtribdServer, routingHeaderMismatch, httpEndpoint, normalizeMcpPath, healthPathFor, DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, DEFAULT_HTTP_PATH, DEFAULT_TOOLS_LIST_TTL_MS, } from './http-host.js';
27
+ export { createAtribdRuntime, createAtribdHttpProxyRuntime, } from './stdio.js';
28
+ function envString(...names) {
29
+ for (const name of names) {
30
+ const value = process.env[name];
31
+ if (value !== undefined && value !== '')
32
+ return value;
33
+ }
34
+ return undefined;
35
+ }
36
+ const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
37
+ function envFlag(...names) {
38
+ const raw = envString(...names);
39
+ return raw !== undefined && TRUE_ENV_VALUES.has(raw.trim().toLowerCase());
40
+ }
41
+ function requireArg(argv, index, flag) {
42
+ const value = argv[index];
43
+ if (value === undefined)
44
+ throw new Error(`${flag} requires a value`);
45
+ return value;
46
+ }
47
+ function parsePort(raw) {
48
+ const n = Number(raw);
49
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
50
+ throw new Error('--port must be an integer from 0 to 65535');
51
+ }
52
+ return n;
53
+ }
54
+ function parseOptionalPort(raw) {
55
+ if (raw === undefined || raw === '')
56
+ return undefined;
57
+ return parsePort(raw);
58
+ }
59
+ function parsePositiveInt(raw, name) {
60
+ const n = Number(raw);
61
+ if (!Number.isInteger(n) || n <= 0) {
62
+ throw new Error(`${name} must be a positive integer`);
63
+ }
64
+ return n;
65
+ }
66
+ function parseOptionalPositiveInt(raw, name) {
67
+ if (raw === undefined || raw === '')
68
+ return undefined;
69
+ return parsePositiveInt(raw, name);
70
+ }
71
+ function normalizeHttpEndpoint(raw, flag) {
72
+ try {
73
+ const url = new URL(raw);
74
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
75
+ throw new Error('unsupported protocol');
76
+ }
77
+ return url.toString();
78
+ }
79
+ catch {
80
+ throw new Error(`${flag} must be an absolute HTTP URL`);
81
+ }
82
+ }
83
+ function deprecatedSessionIdleNotice(source) {
84
+ process.stderr.write(`atribd: ${source} is ignored; the stateless daemon has no HTTP sessions to expire\n`);
85
+ }
86
+ export function parseCliOptions(argv) {
87
+ const options = {
88
+ transport: 'stdio',
89
+ host: envString('ATRIBD_HTTP_HOST', 'ATRIB_PRIMITIVES_HTTP_HOST') ?? DEFAULT_HTTP_HOST,
90
+ port: parseOptionalPort(envString('ATRIBD_HTTP_PORT', 'ATRIB_PRIMITIVES_HTTP_PORT')) ??
91
+ DEFAULT_HTTP_PORT,
92
+ path: envString('ATRIBD_HTTP_PATH', 'ATRIB_PRIMITIVES_HTTP_PATH') ?? DEFAULT_HTTP_PATH,
93
+ endpoint: envString('ATRIBD_HTTP_ENDPOINT', 'ATRIB_PRIMITIVES_HTTP_ENDPOINT') ??
94
+ httpEndpoint(DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, DEFAULT_HTTP_PATH),
95
+ json: false,
96
+ toolTimeoutMs: parseOptionalPositiveInt(envString('ATRIBD_TOOL_TIMEOUT_MS', 'ATRIB_PRIMITIVES_TOOL_TIMEOUT_MS'), 'ATRIBD_TOOL_TIMEOUT_MS') ?? DEFAULT_TOOL_TIMEOUT_MS,
97
+ toolsListTtlMs: parseOptionalPositiveInt(envString('ATRIBD_TOOLS_LIST_TTL_MS'), 'ATRIBD_TOOLS_LIST_TTL_MS') ??
98
+ DEFAULT_TOOLS_LIST_TTL_MS,
99
+ ambientContext: envFlag('ATRIBD_AMBIENT_CONTEXT'),
100
+ help: false,
101
+ };
102
+ // Deprecated session-era configuration: honored as a no-op with a
103
+ // one-line stderr notice, never a fatal error (§5.8 posture toward
104
+ // configuration drift).
105
+ if (envString('ATRIB_PRIMITIVES_SESSION_IDLE_MS') !== undefined) {
106
+ deprecatedSessionIdleNotice('ATRIB_PRIMITIVES_SESSION_IDLE_MS');
107
+ }
108
+ for (let i = 0; i < argv.length; i++) {
109
+ const arg = argv[i];
110
+ if (arg === '--help' || arg === '-h') {
111
+ options.help = true;
112
+ }
113
+ else if (arg === '--http') {
114
+ options.transport = 'streamable-http';
115
+ }
116
+ else if (arg === '--transport') {
117
+ const value = requireArg(argv, ++i, '--transport');
118
+ if (value !== 'stdio' && value !== 'streamable-http' && value !== 'stdio-http-proxy') {
119
+ throw new Error('--transport must be stdio, streamable-http, or stdio-http-proxy');
120
+ }
121
+ options.transport = value;
122
+ }
123
+ else if (arg === '--endpoint') {
124
+ options.endpoint = requireArg(argv, ++i, '--endpoint');
125
+ }
126
+ else if (arg === '--host') {
127
+ options.host = requireArg(argv, ++i, '--host');
128
+ }
129
+ else if (arg === '--port') {
130
+ options.port = parsePort(requireArg(argv, ++i, '--port'));
131
+ }
132
+ else if (arg === '--path') {
133
+ options.path = requireArg(argv, ++i, '--path');
134
+ }
135
+ else if (arg === '--json') {
136
+ options.json = true;
137
+ }
138
+ else if (arg === '--tool-timeout-ms') {
139
+ options.toolTimeoutMs = parsePositiveInt(requireArg(argv, ++i, '--tool-timeout-ms'), '--tool-timeout-ms');
140
+ }
141
+ else if (arg === '--tools-list-ttl-ms') {
142
+ options.toolsListTtlMs = parsePositiveInt(requireArg(argv, ++i, '--tools-list-ttl-ms'), '--tools-list-ttl-ms');
143
+ }
144
+ else if (arg === '--ambient-context') {
145
+ options.ambientContext = true;
146
+ }
147
+ else if (arg === '--session-idle-ms') {
148
+ requireArg(argv, ++i, '--session-idle-ms');
149
+ deprecatedSessionIdleNotice('--session-idle-ms');
150
+ }
151
+ else {
152
+ throw new Error(`unknown argument: ${arg}`);
153
+ }
154
+ }
155
+ options.path = normalizeMcpPath(options.path);
156
+ options.endpoint = normalizeHttpEndpoint(options.endpoint, '--endpoint');
157
+ return options;
158
+ }
159
+ function usage() {
160
+ return `Usage:
161
+ atribd [--transport stdio]
162
+ atribd --transport streamable-http [--host 127.0.0.1] [--port 8796] [--path /mcp]
163
+ atribd --transport stdio-http-proxy --endpoint http://127.0.0.1:8796/mcp
164
+
165
+ Options:
166
+ --http Alias for --transport streamable-http.
167
+ --transport <mode> stdio, streamable-http, or stdio-http-proxy. Defaults to stdio.
168
+ --endpoint <url> HTTP MCP endpoint for stdio-http-proxy mode.
169
+ --host <host> HTTP bind host. Defaults to 127.0.0.1.
170
+ --port <port> HTTP bind port. Defaults to 8796. Use 0 for ephemeral.
171
+ --path <path> HTTP MCP path. Defaults to /mcp.
172
+ --tool-timeout-ms <ms> Bound each primitive tool call. Defaults to 45000.
173
+ --tools-list-ttl-ms <ms> SEP-2549 freshness hint on tools/list. Defaults to 24 hours.
174
+ --ambient-context Opt the HTTP surface back into ambient context discovery
175
+ (D078/D083). Default is explicit-required.
176
+ --session-idle-ms <ms> Deprecated; ignored (the stateless daemon has no sessions).
177
+ --json Print a JSON ready line in HTTP mode.
178
+ --help Print this help.
179
+ `;
180
+ }
181
+ async function main() {
182
+ const options = parseCliOptions(process.argv.slice(2));
183
+ if (options.help) {
184
+ process.stdout.write(usage());
185
+ return;
186
+ }
187
+ if (options.transport === 'streamable-http') {
188
+ const host = await bindAtribdHttpHost({
189
+ host: options.host,
190
+ port: options.port,
191
+ path: options.path,
192
+ jsonReady: options.json,
193
+ toolTimeoutMs: options.toolTimeoutMs,
194
+ toolsListTtlMs: options.toolsListTtlMs,
195
+ ambientContext: options.ambientContext,
196
+ });
197
+ const shutdown = async () => {
198
+ try {
199
+ await host.close();
200
+ }
201
+ finally {
202
+ process.exit(0);
203
+ }
204
+ };
205
+ process.once('SIGINT', shutdown);
206
+ process.once('SIGTERM', shutdown);
207
+ if (!options.json) {
208
+ process.stderr.write(`atribd: listening at ${host.endpoint}\n`);
209
+ }
210
+ return;
211
+ }
212
+ const runtime = options.transport === 'stdio-http-proxy'
213
+ ? await createAtribdHttpProxyRuntime(options.endpoint, {
214
+ toolTimeoutMs: options.toolTimeoutMs,
215
+ })
216
+ : await createAtribdRuntime({
217
+ toolTimeoutMs: options.toolTimeoutMs,
218
+ toolsListTtlMs: options.toolsListTtlMs,
219
+ });
220
+ const shutdown = async () => {
221
+ try {
222
+ await runtime.close();
223
+ }
224
+ finally {
225
+ process.exit(0);
226
+ }
227
+ };
228
+ process.once('SIGINT', shutdown);
229
+ process.once('SIGTERM', shutdown);
230
+ const transport = new StdioServerTransport();
231
+ await runtime.server.connect(transport);
232
+ }
233
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
234
+ main().catch((e) => {
235
+ process.stderr.write(`atribd: fatal ${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
236
+ process.exit(1);
237
+ });
238
+ }
@@ -0,0 +1,22 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { type Tool } from '@modelcontextprotocol/sdk/types.js';
3
+ export interface AtribdRuntime {
4
+ server: Server;
5
+ tools: Tool[];
6
+ toolNames: string[];
7
+ flush(): Promise<void>;
8
+ close(): Promise<void>;
9
+ }
10
+ export interface AtribdRuntimeOptions {
11
+ toolTimeoutMs?: number;
12
+ toolsListTtlMs?: number;
13
+ }
14
+ /** In-process stdio runtime: mounts the primitives and serves them directly. */
15
+ export declare function createAtribdRuntime(options?: AtribdRuntimeOptions): Promise<AtribdRuntime>;
16
+ /**
17
+ * stdio-to-HTTP proxy shim: a lightweight stdio child that forwards MCP
18
+ * calls to a host-owned atribd HTTP endpoint. Works against both the
19
+ * legacy session host and the stateless host (the client's initialize is
20
+ * answered without session issuance and later requests carry no session).
21
+ */
22
+ export declare function createAtribdHttpProxyRuntime(endpoint: string, options?: AtribdRuntimeOptions): Promise<AtribdRuntime>;
package/dist/stdio.js ADDED
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * stdio surfaces of atribd.
4
+ *
5
+ * The stdio shim exists for startup-spawn harnesses that can only spawn
6
+ * per-thread MCP child processes. On this surface the ambient context
7
+ * ladder is unchanged: explicit argument > `_meta` carriers > `ATRIB_CONTEXT_ID`
8
+ * env > harness registry env > fallback file > undefined, per D078/D083.
9
+ * The stateless explicit-required policy applies to the HTTP surface only.
10
+ */
11
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
12
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
15
+ import { callWithToolTimeout, createAtribdBackend, readPackageVersion, DEFAULT_TOOL_TIMEOUT_MS, } from './backend.js';
16
+ import { createAtribdServer, DEFAULT_TOOLS_LIST_TTL_MS } from './http-host.js';
17
+ /** In-process stdio runtime: mounts the primitives and serves them directly. */
18
+ export async function createAtribdRuntime(options = {}) {
19
+ const toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
20
+ const toolsListTtlMs = options.toolsListTtlMs ?? DEFAULT_TOOLS_LIST_TTL_MS;
21
+ const backend = await createAtribdBackend({ toolTimeoutMs });
22
+ const server = createAtribdServer({
23
+ getBackend: async () => backend,
24
+ toolsListTtlMs,
25
+ });
26
+ return {
27
+ server,
28
+ tools: backend.tools,
29
+ toolNames: backend.toolNames,
30
+ flush: backend.flush,
31
+ close: async () => {
32
+ await backend.flush();
33
+ await server.close();
34
+ await backend.close();
35
+ },
36
+ };
37
+ }
38
+ /**
39
+ * stdio-to-HTTP proxy shim: a lightweight stdio child that forwards MCP
40
+ * calls to a host-owned atribd HTTP endpoint. Works against both the
41
+ * legacy session host and the stateless host (the client's initialize is
42
+ * answered without session issuance and later requests carry no session).
43
+ */
44
+ export async function createAtribdHttpProxyRuntime(endpoint, options = {}) {
45
+ const toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
46
+ const upstreamTransport = new StreamableHTTPClientTransport(new URL(endpoint));
47
+ const upstream = new Client({
48
+ name: 'atribd-stdio-http-proxy',
49
+ version: readPackageVersion(),
50
+ });
51
+ await upstream.connect(upstreamTransport);
52
+ const listed = await upstream.listTools();
53
+ const server = new Server({
54
+ name: 'atribd-stdio-http-proxy',
55
+ version: readPackageVersion(),
56
+ }, {
57
+ capabilities: { tools: {} },
58
+ instructions: 'Lightweight stdio proxy for atribd. It forwards MCP calls to a host-owned stateless HTTP daemon.',
59
+ });
60
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listed.tools }));
61
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
62
+ return callWithToolTimeout(request.params.name, toolTimeoutMs, () => upstream.callTool(request.params));
63
+ });
64
+ return {
65
+ server,
66
+ tools: listed.tools,
67
+ toolNames: listed.tools.map((tool) => tool.name),
68
+ flush: async () => { },
69
+ close: async () => {
70
+ await Promise.allSettled([server.close(), upstream.close(), upstreamTransport.close()]);
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Transport adapter boundary (P046).
3
+ *
4
+ * The MCP 2026-07-28 spec removes the `initialize`/`initialized` handshake
5
+ * and the `Mcp-Session-Id` header from Streamable HTTP. The Tier-1
6
+ * TypeScript SDK gate for that transport binds the transport binding, not
7
+ * the daemon core, so the daemon isolates "turn one self-describing HTTP
8
+ * request into MCP server handling" behind this interface. When the SDK
9
+ * ships stateless-transport support, only the adapter internals swap.
10
+ *
11
+ * The current implementation runs the session-era SDK in its documented
12
+ * stateless mode: a fresh `Server` + `StreamableHTTPServerTransport` pair
13
+ * per request, `sessionIdGenerator: undefined` (no session id issued, no
14
+ * session validation, `Mcp-Session-Id` request headers ignored), and JSON
15
+ * responses instead of SSE. A legacy `initialize` POST is answered with a
16
+ * valid capabilities response and no session id.
17
+ */
18
+ import type { IncomingMessage, ServerResponse } from 'node:http';
19
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
+ export interface AtribdTransportAdapter {
21
+ /** Adapter implementation name, surfaced in the health report. */
22
+ readonly name: string;
23
+ /** Highest MCP protocol version the adapter speaks. */
24
+ readonly protocolVersion: string;
25
+ /**
26
+ * Handle one self-describing HTTP request. The caller has already parsed
27
+ * and validated the JSON body (size cap, SEP-2243 routing headers); the
28
+ * adapter owns JSON-RPC dispatch and the response.
29
+ */
30
+ handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody: unknown): Promise<void>;
31
+ }
32
+ export interface SessionSdkStatelessAdapterOptions {
33
+ /**
34
+ * Factory for a per-request MCP server wired to the shared backend.
35
+ * Creating a server per request is what makes any request able to land
36
+ * on any instance: no transport state survives the response.
37
+ */
38
+ serverFactory: () => Server;
39
+ }
40
+ export declare function createSessionSdkStatelessAdapter(options: SessionSdkStatelessAdapterOptions): AtribdTransportAdapter;
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
4
+ export function createSessionSdkStatelessAdapter(options) {
5
+ return {
6
+ name: 'session-sdk-per-request',
7
+ protocolVersion: LATEST_PROTOCOL_VERSION,
8
+ handleRequest: async (req, res, parsedBody) => {
9
+ const server = options.serverFactory();
10
+ const transport = new StreamableHTTPServerTransport({
11
+ sessionIdGenerator: undefined,
12
+ enableJsonResponse: true,
13
+ });
14
+ const cleanup = () => {
15
+ void Promise.allSettled([transport.close(), server.close()]);
16
+ };
17
+ res.once('close', cleanup);
18
+ try {
19
+ await server.connect(transport);
20
+ await transport.handleRequest(req, res, parsedBody);
21
+ }
22
+ finally {
23
+ res.removeListener('close', cleanup);
24
+ cleanup();
25
+ }
26
+ },
27
+ };
28
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@atrib/daemon",
3
+ "version": "0.1.0",
4
+ "description": "Local daemon for atrib. Serves the seven cognitive primitives from one stateless-native process over Streamable HTTP or stdio.",
5
+ "author": "Nader Helmy",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://github.com/creatornader/atrib/tree/main/services/atribd",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/creatornader/atrib.git",
11
+ "directory": "services/atribd"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "bin": {
23
+ "atribd": "./dist/index.js"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.29.0",
30
+ "@atrib/mcp": "0.21.0",
31
+ "@atrib/emit": "0.17.3",
32
+ "@atrib/annotate": "0.2.41",
33
+ "@atrib/recall": "0.14.7",
34
+ "@atrib/summarize": "0.4.23",
35
+ "@atrib/trace": "0.5.21",
36
+ "@atrib/revise": "0.2.41",
37
+ "@atrib/verify-mcp": "0.2.22"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.9.3",
41
+ "tsx": "^4.22.4",
42
+ "typescript": "^6.0.3",
43
+ "vitest": "^4.1.8",
44
+ "zod": "^4.4.3"
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "scripts": {
50
+ "build": "rm -rf dist && tsc && chmod +x dist/index.js",
51
+ "start": "node dist/index.js",
52
+ "dev": "tsx src/index.ts",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }