@lapxo/head 0.2.0 → 0.2.1

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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/TARGET.bound +150 -34
  3. package/dist/src/adopt/adopt.d.ts +2 -2
  4. package/dist/src/adopt/adopt.js +20 -6
  5. package/dist/src/answers/answered.js +1 -1
  6. package/dist/src/compile/pin.d.ts +1 -1
  7. package/dist/src/compile/pin.js +5 -3
  8. package/dist/src/core/answer.d.ts +10 -0
  9. package/dist/src/core/answer.js +1 -1
  10. package/dist/src/core/association.d.ts +16 -0
  11. package/dist/src/core/association.js +34 -0
  12. package/dist/src/core/composition.d.ts +32 -0
  13. package/dist/src/core/composition.js +28 -0
  14. package/dist/src/core/package.d.ts +1 -0
  15. package/dist/src/core/package.js +14 -3
  16. package/dist/src/core/read.d.ts +2 -1
  17. package/dist/src/core/read.js +2 -1
  18. package/dist/src/core/receiver.d.ts +11 -0
  19. package/dist/src/core/receiver.js +122 -0
  20. package/dist/src/core/share-bundle.d.ts +9 -0
  21. package/dist/src/core/share-bundle.js +56 -0
  22. package/dist/src/doors/head.d.ts +1 -0
  23. package/dist/src/doors/head.js +3 -0
  24. package/dist/src/flows/answer.js +7 -3
  25. package/dist/src/flows/call.d.ts +1 -1
  26. package/dist/src/flows/call.js +28 -4
  27. package/dist/src/flows/device.d.ts +11 -5
  28. package/dist/src/flows/device.js +13 -5
  29. package/dist/src/flows/plan.js +1 -1
  30. package/dist/src/index.js +3 -31
  31. package/dist/src/links/inspect.js +1 -1
  32. package/dist/src/onboard/first.js +1 -1
  33. package/dist/src/onboard/owed.js +1 -1
  34. package/dist/src/places/admit.js +1 -1
  35. package/dist/src/places/found.js +3 -1
  36. package/dist/src/ports/adapters/bundle.d.ts +44 -0
  37. package/dist/src/ports/adapters/bundle.js +173 -0
  38. package/dist/src/ports/adapters/configured.d.ts +35 -0
  39. package/dist/src/ports/adapters/configured.js +67 -0
  40. package/dist/src/ports/adapters/mcp.d.ts +59 -0
  41. package/dist/src/ports/adapters/mcp.js +122 -0
  42. package/dist/src/ports/adapters/network.d.ts +12 -0
  43. package/dist/src/ports/adapters/network.js +67 -0
  44. package/dist/src/ports/adapters/protocol.d.ts +6 -0
  45. package/dist/src/ports/adapters/protocol.js +51 -0
  46. package/dist/src/ports/adapters/remote.d.ts +13 -0
  47. package/dist/src/ports/adapters/remote.js +33 -0
  48. package/dist/src/sources/connect.d.ts +1 -0
  49. package/dist/src/sources/connect.js +13 -10
  50. package/dist/src/verbs/ask.js +6 -0
  51. package/dist/src/verbs/do.js +2 -1
  52. package/package.json +5 -4
  53. package/runtime.bundle.json +24026 -0
@@ -0,0 +1,59 @@
1
+ export interface Tool {
2
+ readonly name: string;
3
+ readonly inputSchema: {
4
+ readonly type: 'object';
5
+ };
6
+ readonly annotations?: {
7
+ readonly readOnlyHint?: boolean;
8
+ };
9
+ readonly [key: string]: unknown;
10
+ }
11
+ export interface Options {
12
+ readonly signal: AbortSignal;
13
+ readonly timeout: number;
14
+ }
15
+ export interface ClientPort {
16
+ readonly list: (after: string | undefined, options: Options) => Promise<{
17
+ tools: Tool[];
18
+ next?: string;
19
+ }>;
20
+ readonly callTool: (params: {
21
+ name: string;
22
+ arguments: Record<string, unknown>;
23
+ }, options: Options) => Promise<unknown>;
24
+ readonly close: () => Promise<void>;
25
+ }
26
+ export interface Limits {
27
+ readonly pages: number;
28
+ readonly tools: number;
29
+ readonly bytes: number;
30
+ readonly milliseconds: number;
31
+ }
32
+ export declare const fingerprint: (tools: readonly Tool[]) => {
33
+ tools: readonly Tool[];
34
+ bytes: Buffer<ArrayBuffer>;
35
+ digest: string;
36
+ };
37
+ /** The host supplies an already permitted transport. Discovery observes a catalogue; hints never authorize a call. */
38
+ export declare function withCatalogue<T>(connect: (options: Options) => Promise<ClientPort>, limits: Limits, signal: AbortSignal | undefined, use: (session: {
39
+ readonly read: () => Promise<ReturnType<typeof fingerprint>>;
40
+ readonly call: (pin: string, tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Promise<unknown>;
41
+ }) => Promise<T>): Promise<T>;
42
+ export interface ConfiguredSource {
43
+ readonly protocol: 'head-configured-mcp@1';
44
+ readonly name: string;
45
+ readonly endpoint: string;
46
+ readonly catalogue: string;
47
+ readonly offers: readonly string[];
48
+ }
49
+ export type PermittedFetch = (url: string, init: RequestInit) => Promise<Response>;
50
+ export type RemoteFactory = (endpoint: string, send: PermittedFetch, options: Options) => Promise<ClientPort>;
51
+ /** Public configuration contains no execution, environment, headers or credentials. A URL never supplies a network capability. */
52
+ export declare function configuredSource(value: unknown): ConfiguredSource;
53
+ /** The host grants every connection, including DNS/address policy and receiver-only credentials, through its fetch capability. */
54
+ export declare function withConfiguredSource<T>(configuration: ConfiguredSource, factory: RemoteFactory, permitted: PermittedFetch | undefined, limits: Limits & {
55
+ readonly wireBytes: number;
56
+ }, signal: AbortSignal | undefined, use: (call: (tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Promise<unknown>) => Promise<T>): Promise<T>;
57
+ /** One bounded SDK session; redirects and ambient credentials are never inherited. */
58
+ export declare function transportFor(endpoint: string, permitted: PermittedFetch, wireBytes: number, options: Options): PermittedFetch;
59
+ export { remoteFactory } from './protocol.ts';
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const fingerprint = (tools) => {
3
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : value !== null && typeof value === 'object'
4
+ ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, stable(child)])) : value;
5
+ const bytes = Buffer.from(JSON.stringify(stable([...tools].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0))));
6
+ return { tools, bytes, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` };
7
+ };
8
+ /** The host supplies an already permitted transport. Discovery observes a catalogue; hints never authorize a call. */
9
+ export async function withCatalogue(connect, limits, signal, use) {
10
+ if (Object.values(limits).some(value => !Number.isSafeInteger(value) || value < 1))
11
+ throw Error('mcp/budget');
12
+ let client;
13
+ const stop = AbortSignal.any([AbortSignal.timeout(limits.milliseconds), ...(signal ? [signal] : [])]);
14
+ const close = () => { void client?.close().catch(() => { }); };
15
+ stop.addEventListener('abort', close, { once: true });
16
+ const options = { signal: stop, timeout: limits.milliseconds };
17
+ try {
18
+ stop.throwIfAborted();
19
+ client = await connect(options);
20
+ const connected = client;
21
+ const read = async () => {
22
+ const tools = [], names = new Set(), pages = new Set();
23
+ let after, size = 0;
24
+ for (let page = 0; page < limits.pages; page++) {
25
+ stop.throwIfAborted();
26
+ const result = await connected.list(after, options);
27
+ size += Buffer.byteLength(JSON.stringify(result));
28
+ if (size > limits.bytes || tools.length + result.tools.length > limits.tools)
29
+ throw Error('mcp/budget');
30
+ for (const tool of result.tools) {
31
+ if (names.has(tool.name))
32
+ throw Error('mcp/collision');
33
+ names.add(tool.name);
34
+ tools.push(tool);
35
+ }
36
+ after = result.next;
37
+ if (after === undefined)
38
+ return fingerprint(tools);
39
+ if (!after || pages.has(after))
40
+ throw Error('mcp/page-loop');
41
+ pages.add(after);
42
+ }
43
+ throw Error('mcp/budget');
44
+ };
45
+ return await use({ read, call: async (pin, tool, args, authorize) => {
46
+ const current = await read();
47
+ if (current.digest !== pin)
48
+ throw Error('mcp/catalogue-changed');
49
+ if (!current.tools.some(entry => entry.name === tool))
50
+ throw Error('mcp/tool');
51
+ if (!await authorize())
52
+ throw Error('mcp/not-authorized');
53
+ if ((await read()).digest !== pin)
54
+ throw Error('mcp/catalogue-changed');
55
+ stop.throwIfAborted();
56
+ return connected.callTool({ name: tool, arguments: args }, options);
57
+ } });
58
+ }
59
+ finally {
60
+ stop.removeEventListener('abort', close);
61
+ await client?.close();
62
+ }
63
+ }
64
+ /** Public configuration contains no execution, environment, headers or credentials. A URL never supplies a network capability. */
65
+ export function configuredSource(value) {
66
+ if (!value || typeof value !== 'object' || Array.isArray(value))
67
+ throw Error('mcp/configuration');
68
+ const one = value;
69
+ if (Object.keys(one).sort().join('|') !== 'catalogue|endpoint|name|offers|protocol' || one['protocol'] !== 'head-configured-mcp@1'
70
+ || typeof one['name'] !== 'string' || !/^[a-z][a-z0-9-]*$/.test(one['name']) || typeof one['catalogue'] !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(one['catalogue'])
71
+ || typeof one['endpoint'] !== 'string' || !URL.canParse(one['endpoint']) || !Array.isArray(one['offers']) || !one['offers'].length || one['offers'].some(n => typeof n !== 'string' || !n.trim()) || new Set(one['offers']).size !== one['offers'].length)
72
+ throw Error('mcp/configuration');
73
+ const url = new URL(one['endpoint']);
74
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.href !== one['endpoint'])
75
+ throw Error('mcp/endpoint');
76
+ return { protocol: 'head-configured-mcp@1', name: one['name'], endpoint: url.href, catalogue: one['catalogue'], offers: [...one['offers']] };
77
+ }
78
+ /** The host grants every connection, including DNS/address policy and receiver-only credentials, through its fetch capability. */
79
+ export async function withConfiguredSource(configuration, factory, permitted, limits, signal, use) {
80
+ const source = configuredSource(configuration);
81
+ if (!permitted)
82
+ throw Error('mcp/network-unavailable');
83
+ if (!Number.isSafeInteger(limits.wireBytes) || limits.wireBytes < 1)
84
+ throw Error('mcp/budget');
85
+ if (source.offers.length > limits.tools || Buffer.byteLength(JSON.stringify(source)) > limits.bytes)
86
+ throw Error('mcp/budget');
87
+ return withCatalogue(options => factory(source.endpoint, transportFor(source.endpoint, permitted, limits.wireBytes, options), options), limits, signal, async (session) => {
88
+ const current = await session.read();
89
+ if (current.digest !== source.catalogue || source.offers.some(name => !current.tools.some(tool => tool.name === name)))
90
+ throw Error('mcp/catalogue-changed');
91
+ return use((tool, args, authorize) => {
92
+ if (!source.offers.includes(tool))
93
+ return Promise.reject(Error('mcp/tool'));
94
+ return session.call(source.catalogue, tool, args, authorize);
95
+ });
96
+ });
97
+ }
98
+ /** One bounded SDK session; redirects and ambient credentials are never inherited. */
99
+ export function transportFor(endpoint, permitted, wireBytes, options) {
100
+ if (!Number.isSafeInteger(wireBytes) || wireBytes < 1)
101
+ throw Error('mcp/budget');
102
+ let bytes = 0;
103
+ return async (url, init) => {
104
+ if (url !== endpoint || !['GET', 'POST', 'DELETE'].includes(init.method ?? 'GET'))
105
+ throw Error('mcp/endpoint');
106
+ options.signal.throwIfAborted();
107
+ const got = await permitted(url, { ...init, redirect: 'manual', credentials: 'omit', signal: options.signal });
108
+ if (got.status >= 300 && got.status < 400) {
109
+ await got.body?.cancel();
110
+ throw Error('mcp/redirect');
111
+ }
112
+ const body = got.body?.pipeThrough(new TransformStream({ transform(chunk, controller) {
113
+ options.signal.throwIfAborted();
114
+ bytes += chunk.byteLength;
115
+ if (bytes > wireBytes)
116
+ throw Error('mcp/budget');
117
+ controller.enqueue(chunk);
118
+ } }));
119
+ return new Response(body ?? null, { status: got.status, statusText: got.statusText, headers: got.headers });
120
+ };
121
+ }
122
+ export { remoteFactory } from "./protocol.js";
@@ -0,0 +1,12 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import type { PermittedFetch } from './mcp.ts';
3
+ export interface NetworkPolicy {
4
+ readonly internal?: readonly {
5
+ readonly hostname: string;
6
+ readonly address: string;
7
+ }[];
8
+ readonly ca?: string;
9
+ }
10
+ export declare const publicAddress: (address: string) => boolean;
11
+ /** Receiver-owned transport: payloads cannot supply DNS policy, private bindings, trust roots or credentials. */
12
+ export declare function networkFor(endpoint: string, policy?: NetworkPolicy, resolve?: typeof lookup): PermittedFetch;
@@ -0,0 +1,67 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { BlockList, isIP } from 'node:net';
3
+ import { request } from 'node:https';
4
+ import { Readable } from 'node:stream';
5
+ const excluded = new BlockList();
6
+ for (const [net, bits] of [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]])
7
+ excluded.addSubnet(net, bits);
8
+ for (const [net, bits] of [['2001::', 23], ['2001:db8::', 32], ['2002::', 16], ['3fff::', 20]])
9
+ excluded.addSubnet(net, bits, 'ipv6');
10
+ const global = new BlockList();
11
+ global.addSubnet('2000::', 3, 'ipv6');
12
+ export const publicAddress = (address) => isIP(address) === 4 ? !excluded.check(address) : isIP(address) === 6 && global.check(address, 'ipv6') && !excluded.check(address, 'ipv6');
13
+ /** Receiver-owned transport: payloads cannot supply DNS policy, private bindings, trust roots or credentials. */
14
+ export function networkFor(endpoint, policy = {}, resolve = lookup) {
15
+ const target = new URL(endpoint);
16
+ if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash || target.href !== endpoint)
17
+ throw Error('mcp/endpoint');
18
+ return async (url, init) => {
19
+ if (url !== endpoint || !['GET', 'POST', 'DELETE'].includes(init.method ?? 'GET') || init.redirect !== 'manual' || init.credentials !== 'omit' || !init.signal)
20
+ throw Error('mcp/network-policy');
21
+ const signal = init.signal;
22
+ signal.throwIfAborted();
23
+ const host = target.hostname.replace(/^\[|\]$/g, '');
24
+ let abort;
25
+ const stopped = new Promise((_, reject) => { abort = () => reject(Error('mcp/cancelled')); signal.addEventListener('abort', abort, { once: true }); });
26
+ let addresses;
27
+ try {
28
+ addresses = await Promise.race([resolve(host, { all: true, verbatim: true }), stopped]);
29
+ }
30
+ finally {
31
+ signal.removeEventListener('abort', abort);
32
+ }
33
+ signal.throwIfAborted();
34
+ if (!addresses.length || addresses.some(({ address }) => !publicAddress(address) && !policy.internal?.some(one => one.hostname === host && one.address === address)))
35
+ throw Error('mcp/destination');
36
+ const chosen = addresses[0];
37
+ const headers = Object.fromEntries(new Headers(init.headers));
38
+ if (Object.keys(headers).some(k => ['host', 'connection', 'transfer-encoding', 'content-length', 'proxy-authorization', 'cookie'].includes(k)) || Buffer.byteLength(JSON.stringify(headers)) > 16384)
39
+ throw Error('mcp/headers');
40
+ const body = init.body;
41
+ if (body !== undefined && body !== null && typeof body !== 'string' && !(body instanceof Uint8Array))
42
+ throw Error('mcp/request-body');
43
+ if (body && Buffer.byteLength(body) > 16777216)
44
+ throw Error('mcp/request-budget');
45
+ return new Promise((resolveResponse, reject) => {
46
+ const req = request(target, { method: init.method, headers, agent: false, signal, rejectUnauthorized: true, maxHeaderSize: 16384, ...(policy.ca ? { ca: policy.ca } : {}), family: chosen.family,
47
+ lookup: (_hostname, options, done) => options.all ? done(null, [chosen]) : done(null, chosen.address, chosen.family) }, response => {
48
+ const responseHeaders = new Headers();
49
+ for (const [name, value] of Object.entries(response.headers))
50
+ if (value !== undefined)
51
+ for (const item of Array.isArray(value) ? value : [value])
52
+ responseHeaders.append(name, item);
53
+ const status = response.statusCode ?? 500;
54
+ if ([204, 205, 304].includes(status)) {
55
+ response.resume();
56
+ resolveResponse(new Response(null, { status, headers: responseHeaders }));
57
+ }
58
+ else
59
+ resolveResponse(new Response(Readable.toWeb(response), { status, headers: responseHeaders }));
60
+ });
61
+ req.on('error', () => reject(Error(signal.aborted ? 'mcp/cancelled' : 'mcp/transport')));
62
+ if (body)
63
+ req.write(body);
64
+ req.end();
65
+ });
66
+ };
67
+ }
@@ -0,0 +1,6 @@
1
+ import { type Said } from '../../core/lock.ts';
2
+ import type { RemoteFactory } from './mcp.ts';
3
+ /** Incoming and outgoing SDK bindings; application decisions stay behind the existing ports. */
4
+ export declare function serve(lock: readonly Said[], root: string, ext: string, doorName: string): Promise<void>;
5
+ /** Concrete outgoing SDK transport. Every network operation still goes through the separately permitted host capability. */
6
+ export declare const remoteFactory: RemoteFactory;
@@ -0,0 +1,51 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { Client as RemoteClient } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
+ import { aboutOf, valueOf } from "../../core/lock.js";
7
+ import { fill } from "../../core/surface.js";
8
+ import { shapeOf, shapeSchema } from "../surface.js";
9
+ import { folderOf } from "../place.js";
10
+ import { askedOf, headOf } from "../../doors/head.js";
11
+ /** Incoming and outgoing SDK bindings; application decisions stay behind the existing ports. */
12
+ export async function serve(lock, root, ext, doorName) {
13
+ const offered = (n) => (n ? { listChanged: false } : undefined);
14
+ const head = headOf(lock, root, ext, {
15
+ can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }),
16
+ roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()), name: doorName,
17
+ }), surface = head.surface;
18
+ const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) }, instructions: aboutOf(lock, 'prose/en/instructions') });
19
+ const schemaOf = (fields) => ({ type: 'object',
20
+ properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
21
+ if (surface.tools.length)
22
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
23
+ inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
24
+ if (surface.tools.length)
25
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => ((answer) => (answer === undefined ? { isError: true, content: [] } : { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) }))(await head.call(request.params.name, request.params.arguments, extra.signal)));
26
+ if (surface.resources.length)
27
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
28
+ if (surface.resources.length)
29
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
30
+ if (surface.resources.length)
31
+ server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => ((got) => ({ contents: got === undefined ? [] : [{ uri: request.params.uri, mimeType: got.mime, text: got.lines.join('\n') }] }))(await head.resource(request.params.uri, extra.signal)));
32
+ if (surface.prompts.length)
33
+ server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: surface.prompts.map((p) => ({ name: p.name, description: p.about, arguments: p.fields.map((f) => ({ name: f.name, description: f.about, required: f.required })) })) }));
34
+ if (surface.prompts.length)
35
+ server.setRequestHandler(GetPromptRequestSchema, (request) => ((p) => ({ messages: p === undefined ? [] : [{ role: 'user', content: { type: 'text', text: [p.text, p.line].filter(Boolean).map((t) => fill(t, askedOf(request.params.arguments))).join('\n') } }] }))(surface.prompts.find((one) => one.name === request.params.name)));
36
+ await server.connect(new StdioServerTransport());
37
+ }
38
+ /** Concrete outgoing SDK transport. Every network operation still goes through the separately permitted host capability. */
39
+ export const remoteFactory = async (endpoint, send, options) => {
40
+ const client = new RemoteClient({ name: 'head', version: '0' });
41
+ const transport = new StreamableHTTPClientTransport(new URL(endpoint), { fetch: (input, init) => send(String(input), init ?? {}), reconnectionOptions: { maxRetries: 0, maxReconnectionDelay: 0, initialReconnectionDelay: 0, reconnectionDelayGrowFactor: 1 } });
42
+ try {
43
+ await client.connect(transport, options);
44
+ }
45
+ catch (error) {
46
+ await client.close();
47
+ throw error;
48
+ }
49
+ return { list: async (after, opts) => { const got = await client.listTools(after ? { cursor: after } : {}, opts); return { tools: got.tools, ...(got.nextCursor ? { next: got.nextCursor } : {}) }; },
50
+ callTool: (params, opts) => client.callTool(params, undefined, opts), close: () => client.close() };
51
+ };
@@ -0,0 +1,13 @@
1
+ import { type ConfiguredSource, type Limits, type PermittedFetch } from './mcp.ts';
2
+ import type { Prepared } from './request.ts';
3
+ export interface RemotePort {
4
+ readonly discover: (endpoint: string) => Promise<{
5
+ readonly tools: readonly import('./mcp.ts').Tool[];
6
+ readonly digest: string;
7
+ }>;
8
+ readonly prepare: (source: ConfiguredSource, tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Prepared;
9
+ }
10
+ /** This port owns no permission. Callers bind the receiver's consent and existing access flow; SDK hints grant nothing. */
11
+ export declare function remoteAt(limits: Limits & {
12
+ readonly wireBytes: number;
13
+ }, signal?: AbortSignal, network?: (endpoint: string) => PermittedFetch): RemotePort;
@@ -0,0 +1,33 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { transportFor, withCatalogue, withConfiguredSource, remoteFactory } from "./mcp.js";
3
+ import { networkFor } from "./network.js";
4
+ /** This port owns no permission. Callers bind the receiver's consent and existing access flow; SDK hints grant nothing. */
5
+ export function remoteAt(limits, signal, network = networkFor) {
6
+ return {
7
+ discover: endpoint => withCatalogue(options => remoteFactory(endpoint, transportFor(endpoint, network(endpoint), limits.wireBytes, options), options), limits, signal, session => session.read()),
8
+ prepare: (source, tool, args, authorize) => {
9
+ if (!source.offers.includes(tool))
10
+ return { refused: 'mcp-tool' };
11
+ const sentArgs = JSON.parse(JSON.stringify(args));
12
+ const key = createHash('sha256').update(JSON.stringify([source.endpoint, source.catalogue, tool, sentArgs])).digest('hex');
13
+ return { key, describe: `${source.endpoint} · ${tool} · sha256:${key}`, send: async (attachment, most) => {
14
+ if (!Number.isSafeInteger(most) || most < 1 || Object.keys(attachment?.query ?? {}).length)
15
+ return { kind: 'failed', why: 'mcp/access-location' };
16
+ try {
17
+ const permitted = network(source.endpoint);
18
+ const result = await withConfiguredSource(source, remoteFactory, (url, init) => {
19
+ const headers = new Headers(init.headers);
20
+ for (const [key, value] of Object.entries(attachment?.headers ?? {}))
21
+ headers.set(key, value);
22
+ return permitted(url, { ...init, headers });
23
+ }, limits, signal, call => call(tool, sentArgs, authorize));
24
+ const bytes = Buffer.from(JSON.stringify(result));
25
+ return bytes.length > most ? { kind: 'over' } : { kind: 'got', status: 200, bytes, type: 'application/json' };
26
+ }
27
+ catch (error) {
28
+ return { kind: 'failed', why: error instanceof Error && /^mcp\/[a-z-]+$/.test(error.message) ? error.message : 'mcp/transport' };
29
+ }
30
+ } };
31
+ },
32
+ };
33
+ }
@@ -17,5 +17,6 @@ type Connection = {
17
17
  readonly connected: boolean;
18
18
  };
19
19
  type Laid = (source: string, document: string, doc: Read, overlay: Read | undefined, started: number) => Promise<Connection>;
20
+ export declare const received: (context: Context, place: string, document: string, bytes: Uint8Array, named: string, laid: Laid, account: string, publicFiles?: ReadonlyMap<string, Uint8Array | undefined>) => Promise<readonly string[]>;
20
21
  export declare const laidIn: (context: Context, place: string, account: string) => Laid;
21
22
  export {};
@@ -1,3 +1,5 @@
1
+ import { discoverConfigured, configuredRefusal, configuredProtocol } from "../ports/adapters/configured.js";
2
+ import { receiverConnect } from "../core/receiver.js";
1
3
  import { canonical, EXTENSION } from '@lapxo/topos/wire';
2
4
  import { inPlace, placeOf, unless } from "../core/answer.js";
3
5
  import { members, valueOf } from "../core/lock.js";
@@ -24,8 +26,8 @@ import { attachedFor } from "./access.js";
24
26
  * its pins rest in, its pin held to the package's; one that differs is refused by name, and the place holds what it
25
27
  * compiled. Nothing the sharing place was granted comes with it: access is asked here, issuer by issuer.
26
28
  */
27
- export const connected = async (asked, context, covered = false) => (await receivedBirth(asked, context)) ?? (members(valueOf(context.lock, 'link/prefixes')).some(prefix => (asked['document'] ?? '').startsWith(prefix)) ? linked(asked, context)
28
- : ((got) => ('said' in got ? got.said : connectedAs(got.asked, context, covered)))(await resolved(asked, context)));
29
+ export const connected = async (asked, context, covered = false) => (await receiverConnect(asked, context)) ?? (await receivedBirth(asked, context)) ?? (members(valueOf(context.lock, 'link/prefixes')).some(prefix => (asked['document'] ?? '').startsWith(prefix)) ? linked(asked, context)
30
+ : ((got) => ('said' in got ? got.said : connectedAs(got.asked, context, covered)))(asked['protocol'] === configuredProtocol.id ? { asked } : await resolved(asked, context)));
29
31
  const connectedAs = async (asked, context, covered = false) => (asked['source'] && !(await placeOf(asked, context)) ? firstRun(asked, context, (dir, covered) => connected({ ...asked, place: dir }, context, covered))
30
32
  : ((started) => inPlace(asked, context, async (place) => {
31
33
  const say = sayIn(context.lock), digest = digestIn(context.lock), { documentOf, fetchedAs, kindOf } = context.files.read;
@@ -35,7 +37,7 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
35
37
  return [say('parent-lock', { place, parent: source || document })];
36
38
  if (!source && !lock)
37
39
  return [say('missing-param', { operation: 'connect', param: 'source' })];
38
- const root = rootIn(context, place);
40
+ const root = await rootIn(context, place);
39
41
  if ('said' in root)
40
42
  return root.said;
41
43
  if (named && !/^sha256:[0-9a-f]{64}$/.test(named))
@@ -49,7 +51,7 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
49
51
  const got = via ? await attachedFor(context, place, via) : undefined;
50
52
  if (got && 'said' in got)
51
53
  return got.said;
52
- const [read, overlay] = [got ? await fetchedAs(context, document, via, got, named, accept) : await documentOf(context, place, document, named), asked['overlay'] ? await documentOf(context, place, asked['overlay']) : undefined];
54
+ const [read, overlay] = [asked['protocol'] === configuredProtocol.id ? await discoverConfigured(context, asked, source) : got ? await fetchedAs(context, document, via, got, named, accept) : await documentOf(context, place, document, named), asked['overlay'] ? await documentOf(context, place, asked['overlay']) : undefined];
53
55
  if (read !== undefined && 'said' in read)
54
56
  return read.said;
55
57
  if (read === undefined || (asked['overlay'] && overlay === undefined))
@@ -57,13 +59,14 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
57
59
  if (named && digest(read.bytes) !== named)
58
60
  return [say('document-digest', { document, digest: named, got: digest(read.bytes) })];
59
61
  return `.${read.kind}` === EXTENSION ? [say('parent-lock', { place, parent: source })] : (await laidIn(context, place, root.account)(source, document, read, overlay, started)).said;
60
- }))(context.machine()));
61
- const received = async (context, place, document, bytes, named, laid, account) => {
62
- const [say, got] = [sayIn(context.lock), unpacked(context, document, bytes, await context.files.read.besideOf(context, document, linesOf(bytes)))];
62
+ }))(context.machine())).catch(error => { if (asked['protocol'] === configuredProtocol.id || error instanceof Error && error.message.startsWith(configuredProtocol.failurePrefix))
63
+ return configuredRefusal(context, error); throw error; });
64
+ export const received = async (context, place, document, bytes, named, laid, account, publicFiles) => {
65
+ const [say, got] = [sayIn(context.lock), unpacked(context, document, bytes, publicFiles ?? await context.files.read.besideOf(context, document, linesOf(bytes)))];
63
66
  if (!got.sources)
64
67
  return got.said ?? [];
65
68
  const [names, said, before] = [got.sources.map((s) => s.source).join(', '), [], await context.policy(place)];
66
- const refused = coveredIn(context, 'refused' in before ? [] : before.lines, got.sources, named) ? undefined : await unless(context, say('elicit-receive', { place, package: named, sources: names || '—', open: (got.open ?? []).map((o) => o.source).join(', ') || '—' }));
69
+ const refused = publicFiles || coveredIn(context, 'refused' in before ? [] : before.lines, got.sources, named) ? undefined : await unless(context, say('elicit-receive', { place, package: named, sources: names || '—', open: (got.open ?? []).map((o) => o.source).join(', ') || '—' }));
67
70
  if (refused)
68
71
  return refused;
69
72
  for (const s of got.sources)
@@ -77,8 +80,8 @@ export const laidIn = (context, place, account) => (source, document, doc, overl
77
80
  const say = sayIn(context.lock), v = vocabOf(context.lock), word = (k) => valueOf(context.lock, k), digest = digestIn(context.lock);
78
81
  const lands = (lot) => context.landRoot(place, lot, account);
79
82
  const line = (scope, measure, value, more = {}) => canonical({ at: 'policy:head/connect', by: 'target', form: 'alphabet', measure, role: 'writes', scope, value, ...more });
80
- const pin = pinIn(context, place, lands, line);
81
- const worlds = await adopted(context, place, lands, line);
83
+ const configured = configuredProtocol.connection(doc.bytes, source, overlay !== undefined, v);
84
+ const [pin, worlds] = [pinIn(context, place, lands, line, configured?.verify), await adopted(context, place, lands, line, configured?.worlds ?? [])];
82
85
  if ('refused' in worlds)
83
86
  return { said: [say('not-landed', { why: worlds.refused })], connected: false };
84
87
  const laid = [[`${word('place/connected')}/${source}.${doc.kind}`, doc.bytes], ...(overlay ? [[`${word('place/connected')}/${source}.overlay.${overlay.kind}`, overlay.bytes]] : [])];
@@ -1,3 +1,4 @@
1
+ import { receiverQuestion } from "../core/receiver.js";
1
2
  import { join } from 'node:path';
2
3
  import { LOCK } from '@lapxo/topos/wire';
3
4
  import { answerOf, placeOf } from "../core/answer.js";
@@ -10,6 +11,11 @@ import { viewed } from "../claims/view.js";
10
11
  import { cardAt } from "./card.js";
11
12
  /** Route a question at the declared resolution, then attach the card of the place it read. */
12
13
  export const asked = async (asked, context) => {
14
+ const packaged = await receiverQuestion(asked, context);
15
+ if (packaged && 'said' in packaged)
16
+ return packaged.said;
17
+ if (packaged)
18
+ asked = { ...asked, place: packaged.place };
13
19
  const why = asked['why'] === 'true', at = (form) => ((v) => (v && v !== valueOf(context.lock, 'resolution/own') ? v : undefined))(valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`));
14
20
  const read = async (form, tool, more = {}) => [...(why && at(form) === undefined ? [sayIn(context.lock)('why-undeclared', { form })] : []),
15
21
  ...await (await answerOf('tools', tool))({ ...asked, ...more, at: at(form) }, context)];
@@ -2,6 +2,7 @@ import { answerOf, placeOf } from "../core/answer.js";
2
2
  import { members, valueOf } from "../core/lock.js";
3
3
  import { sayIn } from "../core/surface.js";
4
4
  import { cardAt } from "./card.js";
5
+ import { LOCK } from '@lapxo/topos/wire';
5
6
  /**
6
7
  * What a person does, carried out by the action it names and only on the card it was asked on: an act on a place that
7
8
  * moved since is refused before anything is done. An action the place does not allow is said not to be in the card, by
@@ -16,7 +17,7 @@ export const done = async (asked, context) => {
16
17
  const place = asked['place'] || asked['dir'] || await placeOf(asked, context), held = await cardAt(context, place);
17
18
  if ('said' in held)
18
19
  return held.said;
19
- if (held.card !== asked['card'])
20
+ if (held.card !== asked['card'] && !(action === 'place' && place !== undefined && !context.files.has(place, LOCK) && !context.files.has(place, '.bound')))
20
21
  return [say('card-stale', { action, place: place ?? '', card: asked['card'] ?? '' })];
21
22
  const answer = await (await answerOf('tools', action))(asked, context);
22
23
  const [bound, own] = [members(word('do/not-in-card/bound')), members(word('do/not-in-card/head'))];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapxo/head",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "An MCP server that folds: it runs bound as a process, hands a model what bound printed, and lands what it read as receipts.",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "files": [
19
19
  "dist/src",
20
20
  "TARGET.bound",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "runtime.bundle.json"
22
23
  ],
23
24
  "scripts": {
24
25
  "build": "tsc -p . --incremental false --tsBuildInfoFile null",
@@ -28,10 +29,10 @@
28
29
  "dependencies": {
29
30
  "@lapxo/bound": "0.2.8",
30
31
  "@lapxo/topos": "0.2.4",
31
- "@modelcontextprotocol/sdk": "1.32.1"
32
+ "@modelcontextprotocol/sdk": "1.32.1",
33
+ "@anthropic-ai/mcpb": "2.1.2"
32
34
  },
33
35
  "devDependencies": {
34
- "@anthropic-ai/mcpb": "2.1.2",
35
36
  "@types/node": "22.20.5",
36
37
  "typescript": "5.9.3"
37
38
  }