@xenosystem/capability-mcp 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @xenosystem/capability-mcp
2
+
3
+ **The conversation inherits the tools of the applications you have open.**
4
+
5
+ Every XENO application already exposes what it can do over an authenticated loopback HTTP
6
+ surface (`XENO AGENT CAPABILITY - SPEC.md` §10.1). The agent conversation already accepts
7
+ extra tools — as MCP. Nothing translated between the two, so an agent running inside Canvas
8
+ could not draw a rectangle while Canvas exposed 73 operations three inches away.
9
+
10
+ This is that translation: one adapter, an MCP stdio server, that discovers every running XENO
11
+ application and presents its capabilities as tools named `<app>_<domain>_<verb>`.
12
+
13
+ ```
14
+ Canvas / Pixel / Motion / Browser / Workflow / Engine / …
15
+ │ GET /capabilities · POST /capabilities/<name> (bearer token, loopback only)
16
+ ▼
17
+ xeno-capability-mcp ← this package
18
+ │ MCP over stdio
19
+ ▼
20
+ PluginMcpManagerService ← the existing seam, with its existing approval gate
21
+ │
22
+ ▼
23
+ the agent
24
+ ```
25
+
26
+ ## Try it
27
+
28
+ ```bash
29
+ node dist/bin.js --list # what is running right now, and what is not
30
+ npm run smoke:live # spawn the real binary, drive a real app, assert it changed
31
+ ```
32
+
33
+ `--list` against a machine with Canvas open:
34
+
35
+ ```
36
+ 73 capabilities from 1 running app(s): canvas
37
+ not available: motion — not running (stale descriptor, pid 188488)
38
+ …
39
+ ```
40
+
41
+ ## Five things that are deliberate
42
+
43
+ **1. Discovery is live, and re-run on every `tools/list`.** A descriptor in
44
+ `~/.xeno/capability-servers/` outlives the process that wrote it, so the *steady state* of
45
+ that directory is stale — measured 2026-09-08, all eight descriptors named dead pids. An
46
+ adapter that trusted the file would offer an agent operations that cannot run.
47
+
48
+ **2. A closed application is ABSENT, not FAILING.** `canvas is not available: not running.
49
+ Ask the user to open canvas, then retry` and `no such tool` lead to opposite next actions, and
50
+ an agent told only the second concludes the capability does not exist.
51
+
52
+ **3. The application's own refusal stays a refusal.** The §8 envelope is passed through
53
+ uninterpreted and `isError` is derived from it. Returning `{ ok: false }` as a successful tool
54
+ result is the fabricated-success shape that let `xeno-workflow` ship 41 nodes reporting
55
+ success while doing nothing.
56
+
57
+ **4. The token never leaves this process.** Descriptors are `0600` and carry a per-launch
58
+ bearer token. It goes on the wire and into no tool result — asserted by a test.
59
+
60
+ **5. `inputSchema` is a COMPLETE JSON Schema**, per the MCP spec. `@xenosystem/agent-sdk` used
61
+ to read it as a bare properties map, so every MCP tool in every XENO product reached the model
62
+ with parameters named `type`, `properties` and `required` and the real ones invisible. Fixed
63
+ in that repo's `src/mcp/tool-adapter.ts`; do not "fix" this end to match an old client.
64
+
65
+ ## The envelope divergence this absorbs
66
+
67
+ Measured across the eight shipped capability servers on 2026-09-08:
68
+
69
+ | application | `GET /capabilities` body | schema field |
70
+ |---|---|---|
71
+ | canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
72
+ | engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
73
+
74
+ Eight applications each growing their own MCP endpoint would be eight places to absorb that,
75
+ drifting independently. Here it is one function — and any future application that merely has a
76
+ capability server is covered without writing anything.
77
+
78
+ ## Ports
79
+
80
+ Read the port from the descriptor, never from `XENO PORT ALLOCATIONS.md`. That document
81
+ records a *preference*; an application that cannot get its preferred port binds another and
82
+ publishes what it actually obtained. Canvas has been observed on 7333, 7395 and 30611.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The stdio entry point `PluginMcpManagerService` spawns.
4
+ *
5
+ * `--list` prints what is reachable right now and exits, because the first question anyone
6
+ * asks of this thing is "is anything running", and answering it should not require speaking
7
+ * JSON-RPC by hand.
8
+ */
9
+ import { discover } from './catalogue.js';
10
+ import { serve } from './mcpServer.js';
11
+ const argv = process.argv.slice(2);
12
+ if (argv.includes('--list')) {
13
+ const catalogue = await discover();
14
+ const apps = [...new Set(catalogue.entries.map((entry) => entry.app))];
15
+ process.stdout.write(`${catalogue.entries.length} capabilities from ${apps.length} running app(s): ${apps.join(', ') || 'none'}\n`);
16
+ for (const { app, reason } of catalogue.unreachable)
17
+ process.stdout.write(` not available: ${app} — ${reason}\n`);
18
+ process.exit(0);
19
+ }
20
+ await serve();
21
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAElC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,MAAM,QAAQ,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,sBAAsB,IAAI,CAAC,MAAM,oBAAoB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,CAAA;IACnI,KAAK,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,WAAW;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,MAAM,MAAM,IAAI,CAAC,CAAA;IAClH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AAED,MAAM,KAAK,EAAE,CAAA"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Talking to one XENO application's capability server.
3
+ *
4
+ * `GET /capabilities` and `POST /capabilities/<name>` are the whole surface
5
+ * (`XENO AGENT CAPABILITY - SPEC.md` §10.1), and every app bearer-authenticates with the
6
+ * per-launch token from its descriptor.
7
+ *
8
+ * 🔴 **The eight shipped servers do NOT agree on the response envelope, and that divergence
9
+ * is the reason ONE adapter is the right shape.** Measured 2026-09-08:
10
+ *
11
+ * | app | `GET /capabilities` body | schema field |
12
+ * |---|---|---|
13
+ * | canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
14
+ * | engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
15
+ *
16
+ * Eight apps each growing their own MCP endpoint would be eight places to absorb that; here
17
+ * it is one function, and every future app that merely has a capability server is covered
18
+ * without writing anything.
19
+ *
20
+ * This deliberately does NOT reconcile the divergence upstream: `XENO AGENT CAPABILITY -
21
+ * SPEC.md` §15 makes *exposing* the product's job and *driving* somebody else's, and the
22
+ * products are shipping. Normalising here is the driving half doing its job.
23
+ */
24
+ import type { CapabilityDescriptor } from './descriptors.js';
25
+ /** One operation an application offers, normalised across the envelope divergence above. */
26
+ export interface Capability {
27
+ name: string;
28
+ description: string;
29
+ /** A JSON Schema for the arguments. `{ type: 'object', properties: {} }` when absent. */
30
+ schema: {
31
+ type: 'object';
32
+ properties: Record<string, unknown>;
33
+ required?: string[] | undefined;
34
+ };
35
+ /** `safe` | `undoable` | `irreversible` when the app declares it. */
36
+ reversibility?: string | undefined;
37
+ }
38
+ export interface CapabilityClientOptions {
39
+ fetchImpl?: typeof fetch | undefined;
40
+ timeoutMs?: number | undefined;
41
+ }
42
+ /**
43
+ * Pull the capability array out of whatever envelope this app answered with.
44
+ *
45
+ * Returns `undefined` rather than `[]` when nothing recognisable is there. That difference
46
+ * is load-bearing: an empty array says "this app has no capabilities" and an unreadable body
47
+ * says "this is not one of our servers" — and the second is how a descriptor whose port has
48
+ * been recycled by an unrelated process gets refused instead of advertised.
49
+ */
50
+ export declare function extractCapabilities(body: unknown): unknown[] | undefined;
51
+ /** Normalise one descriptor entry. `argsSchema` and `inputSchema` are both in the wild. */
52
+ export declare function normalizeCapability(entry: unknown): Capability | undefined;
53
+ export declare class CapabilityUnavailable extends Error {
54
+ readonly app: string;
55
+ constructor(app: string, message: string);
56
+ }
57
+ /** A thin, tolerant client for one app's capability server. */
58
+ export declare class CapabilityClient {
59
+ readonly descriptor: CapabilityDescriptor;
60
+ private readonly fetchImpl;
61
+ private readonly timeoutMs;
62
+ constructor(descriptor: CapabilityDescriptor, options?: CapabilityClientOptions);
63
+ private request;
64
+ /**
65
+ * What can this app do right now?
66
+ *
67
+ * This IS the liveness check. A pid says a process exists; only a successful, correctly
68
+ * shaped answer says this app is up, is ours, and honours the token we hold.
69
+ */
70
+ listCapabilities(): Promise<Capability[]>;
71
+ /**
72
+ * Run one capability and return its raw §8 envelope.
73
+ *
74
+ * The envelope is returned UNINTERPRETED. It already distinguishes success from failure
75
+ * with a code the agent can act on, and re-deciding that here would put two opinions in
76
+ * the path that can disagree — the shape that produced `{ success: true }` with nothing
77
+ * done in xeno-workflow's 41 fabricated nodes.
78
+ */
79
+ invoke(name: string, args: Record<string, unknown>): Promise<unknown>;
80
+ }
81
+ //# sourceMappingURL=capabilityClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilityClient.d.ts","sourceRoot":"","sources":["../src/capabilityClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAChG,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B;AAUD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,SAAS,CAUxE;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAkB1E;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM;CAKlB;AAED,+DAA+D;AAC/D,qBAAa,gBAAgB;IAKzB,QAAQ,CAAC,UAAU,EAAE,oBAAoB;IAJ3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAGvB,UAAU,EAAE,oBAAoB,EACzC,OAAO,GAAE,uBAA4B;YAMzB,OAAO;IAoBrB;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAkC/C;;;;;;;OAOG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CA2B5E"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Talking to one XENO application's capability server.
3
+ *
4
+ * `GET /capabilities` and `POST /capabilities/<name>` are the whole surface
5
+ * (`XENO AGENT CAPABILITY - SPEC.md` §10.1), and every app bearer-authenticates with the
6
+ * per-launch token from its descriptor.
7
+ *
8
+ * 🔴 **The eight shipped servers do NOT agree on the response envelope, and that divergence
9
+ * is the reason ONE adapter is the right shape.** Measured 2026-09-08:
10
+ *
11
+ * | app | `GET /capabilities` body | schema field |
12
+ * |---|---|---|
13
+ * | canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
14
+ * | engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
15
+ *
16
+ * Eight apps each growing their own MCP endpoint would be eight places to absorb that; here
17
+ * it is one function, and every future app that merely has a capability server is covered
18
+ * without writing anything.
19
+ *
20
+ * This deliberately does NOT reconcile the divergence upstream: `XENO AGENT CAPABILITY -
21
+ * SPEC.md` §15 makes *exposing* the product's job and *driving* somebody else's, and the
22
+ * products are shipping. Normalising here is the driving half doing its job.
23
+ */
24
+ const DEFAULT_TIMEOUT_MS = 15_000;
25
+ function asObject(value) {
26
+ return value && typeof value === 'object' && !Array.isArray(value)
27
+ ? value
28
+ : undefined;
29
+ }
30
+ /**
31
+ * Pull the capability array out of whatever envelope this app answered with.
32
+ *
33
+ * Returns `undefined` rather than `[]` when nothing recognisable is there. That difference
34
+ * is load-bearing: an empty array says "this app has no capabilities" and an unreadable body
35
+ * says "this is not one of our servers" — and the second is how a descriptor whose port has
36
+ * been recycled by an unrelated process gets refused instead of advertised.
37
+ */
38
+ export function extractCapabilities(body) {
39
+ const root = asObject(body);
40
+ if (!root)
41
+ return Array.isArray(body) ? body : undefined;
42
+ if (Array.isArray(root['capabilities']))
43
+ return root['capabilities'];
44
+ // A manifest object carrying the list one level down.
45
+ const nested = asObject(root['capabilities']);
46
+ if (nested && Array.isArray(nested['capabilities']))
47
+ return nested['capabilities'];
48
+ const data = asObject(root['data']);
49
+ if (data && Array.isArray(data['capabilities']))
50
+ return data['capabilities'];
51
+ return undefined;
52
+ }
53
+ /** Normalise one descriptor entry. `argsSchema` and `inputSchema` are both in the wild. */
54
+ export function normalizeCapability(entry) {
55
+ const record = asObject(entry);
56
+ const name = record?.['name'];
57
+ if (typeof name !== 'string' || !name)
58
+ return undefined;
59
+ const rawSchema = asObject(record?.['argsSchema']) ?? asObject(record?.['inputSchema']) ?? asObject(record?.['schema']);
60
+ const properties = asObject(rawSchema?.['properties']) ?? {};
61
+ const rawRequired = rawSchema?.['required'];
62
+ const required = Array.isArray(rawRequired)
63
+ ? rawRequired.filter((value) => typeof value === 'string')
64
+ : undefined;
65
+ return {
66
+ name,
67
+ description: typeof record?.['description'] === 'string' ? record['description'] : name,
68
+ schema: { type: 'object', properties, ...(required && required.length ? { required } : {}) },
69
+ reversibility: typeof record?.['reversibility'] === 'string' ? record['reversibility'] : undefined,
70
+ };
71
+ }
72
+ export class CapabilityUnavailable extends Error {
73
+ app;
74
+ constructor(app, message) {
75
+ super(message);
76
+ this.app = app;
77
+ this.name = 'CapabilityUnavailable';
78
+ }
79
+ }
80
+ /** A thin, tolerant client for one app's capability server. */
81
+ export class CapabilityClient {
82
+ descriptor;
83
+ fetchImpl;
84
+ timeoutMs;
85
+ constructor(descriptor, options = {}) {
86
+ this.descriptor = descriptor;
87
+ this.fetchImpl = options.fetchImpl ?? fetch;
88
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
89
+ }
90
+ async request(path, init) {
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
93
+ try {
94
+ return await this.fetchImpl(`${this.descriptor.url}${path}`, {
95
+ ...init,
96
+ signal: controller.signal,
97
+ headers: {
98
+ ...init?.headers,
99
+ // The token stays in this process. It is read from a 0600 descriptor and put on
100
+ // the wire; it is never part of a tool result, so it cannot reach a renderer or a
101
+ // model transcript.
102
+ authorization: `Bearer ${this.descriptor.token}`,
103
+ },
104
+ });
105
+ }
106
+ finally {
107
+ clearTimeout(timer);
108
+ }
109
+ }
110
+ /**
111
+ * What can this app do right now?
112
+ *
113
+ * This IS the liveness check. A pid says a process exists; only a successful, correctly
114
+ * shaped answer says this app is up, is ours, and honours the token we hold.
115
+ */
116
+ async listCapabilities() {
117
+ let response;
118
+ try {
119
+ response = await this.request('/capabilities');
120
+ }
121
+ catch (error) {
122
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} is not answering on ${this.descriptor.url}: ${error.message}`);
123
+ }
124
+ if (!response.ok) {
125
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered ${response.status} for GET /capabilities.`);
126
+ }
127
+ let body;
128
+ try {
129
+ body = await response.json();
130
+ }
131
+ catch {
132
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered with non-JSON.`);
133
+ }
134
+ const entries = extractCapabilities(body);
135
+ if (!entries) {
136
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered a body with no capability list — the port may belong to something else now.`);
137
+ }
138
+ return entries
139
+ .map((entry) => normalizeCapability(entry))
140
+ .filter((capability) => capability !== undefined);
141
+ }
142
+ /**
143
+ * Run one capability and return its raw §8 envelope.
144
+ *
145
+ * The envelope is returned UNINTERPRETED. It already distinguishes success from failure
146
+ * with a code the agent can act on, and re-deciding that here would put two opinions in
147
+ * the path that can disagree — the shape that produced `{ success: true }` with nothing
148
+ * done in xeno-workflow's 41 fabricated nodes.
149
+ */
150
+ async invoke(name, args) {
151
+ let response;
152
+ try {
153
+ response = await this.request(`/capabilities/${encodeURIComponent(name)}`, {
154
+ method: 'POST',
155
+ headers: { 'content-type': 'application/json' },
156
+ body: JSON.stringify({ args }),
157
+ });
158
+ }
159
+ catch (error) {
160
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} is not answering: ${error.message}`);
161
+ }
162
+ const text = await response.text();
163
+ if (!response.ok && !text) {
164
+ throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered ${response.status}.`);
165
+ }
166
+ try {
167
+ return JSON.parse(text);
168
+ }
169
+ catch {
170
+ return { ok: false, error: { code: 'internal', message: text.slice(0, 2000) } };
171
+ }
172
+ }
173
+ }
174
+ //# sourceMappingURL=capabilityClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilityClient.js","sourceRoot":"","sources":["../src/capabilityClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Every capability of every RUNNING XENO application, as one tool list.
3
+ *
4
+ * Discovery is deliberately re-run on every `tools/list`. An app the user opens after the
5
+ * conversation started must appear, and an app they close must disappear — a catalogue
6
+ * cached at startup is exactly how an agent ends up offering operations that cannot run.
7
+ */
8
+ import { type Capability } from './capabilityClient.js';
9
+ import { type CapabilityDescriptor } from './descriptors.js';
10
+ export interface CatalogueEntry {
11
+ app: string;
12
+ capability: Capability;
13
+ /** The MCP tool name. App-qualified, and safe for every MCP client. */
14
+ toolName: string;
15
+ }
16
+ export interface Catalogue {
17
+ entries: CatalogueEntry[];
18
+ /**
19
+ * Apps found on disk that could not be reached, with the reason.
20
+ *
21
+ * Kept rather than discarded: "Canvas is not running" and "Canvas has no such capability"
22
+ * are different instructions for an agent, and only the first is recoverable by asking the
23
+ * user to open the app.
24
+ */
25
+ unreachable: {
26
+ app: string;
27
+ reason: string;
28
+ }[];
29
+ }
30
+ /**
31
+ * MCP tool name for one capability.
32
+ *
33
+ * The name is app-qualified so two apps cannot collide and so the agent can say WHICH app it
34
+ * is acting on. Capability names are already qualified in every shipped app
35
+ * (`canvas.document.create`), so the prefix is added only when it is missing — otherwise the
36
+ * tool would read `canvas_canvas_document_create`.
37
+ *
38
+ * Dots become underscores. The consuming SDK's sanitiser permits dots
39
+ * (`/[^A-Za-z0-9_.-]+/`), but Anthropic's tool-name rule is `^[a-zA-Z0-9_-]{1,64}$` and this
40
+ * server is meant to work with any MCP client, so the conservative alphabet is the one that
41
+ * cannot be wrong somewhere else.
42
+ */
43
+ export declare function toolNameFor(app: string, capabilityName: string): string;
44
+ export interface DiscoverOptions {
45
+ dir?: string | undefined;
46
+ fetchImpl?: typeof fetch | undefined;
47
+ timeoutMs?: number | undefined;
48
+ /** Injected so the liveness rule can be tested without spawning processes. */
49
+ isAlive?: ((pid: number | undefined) => boolean) | undefined;
50
+ }
51
+ /** Descriptors whose publishing process still exists. Cheap, and never the whole answer. */
52
+ export declare function candidateDescriptors(options?: DiscoverOptions): {
53
+ candidates: CapabilityDescriptor[];
54
+ dead: {
55
+ app: string;
56
+ reason: string;
57
+ }[];
58
+ };
59
+ /** Build the live catalogue. Apps are probed in parallel; one being down never blocks another. */
60
+ export declare function discover(options?: DiscoverOptions): Promise<Catalogue>;
61
+ /** The description a model sees. It has to carry the app and the risk, not just the verb. */
62
+ export declare function describeEntry(entry: CatalogueEntry): string;
63
+ //# sourceMappingURL=catalogue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogue.d.ts","sourceRoot":"","sources":["../src/catalogue.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE7F,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,UAAU,CAAA;IACtB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB;;;;;;OAMG;IACH,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,GAAG,SAAS,CAAA;CAC7D;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,eAAoB,GAAG;IACnE,UAAU,EAAE,oBAAoB,EAAE,CAAA;IAClC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxC,CASA;AAED,kGAAkG;AAClG,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmChF;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAM3D"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Every capability of every RUNNING XENO application, as one tool list.
3
+ *
4
+ * Discovery is deliberately re-run on every `tools/list`. An app the user opens after the
5
+ * conversation started must appear, and an app they close must disappear — a catalogue
6
+ * cached at startup is exactly how an agent ends up offering operations that cannot run.
7
+ */
8
+ import { CapabilityClient, CapabilityUnavailable } from './capabilityClient.js';
9
+ import { processIsAlive, readDescriptors } from './descriptors.js';
10
+ /**
11
+ * MCP tool name for one capability.
12
+ *
13
+ * The name is app-qualified so two apps cannot collide and so the agent can say WHICH app it
14
+ * is acting on. Capability names are already qualified in every shipped app
15
+ * (`canvas.document.create`), so the prefix is added only when it is missing — otherwise the
16
+ * tool would read `canvas_canvas_document_create`.
17
+ *
18
+ * Dots become underscores. The consuming SDK's sanitiser permits dots
19
+ * (`/[^A-Za-z0-9_.-]+/`), but Anthropic's tool-name rule is `^[a-zA-Z0-9_-]{1,64}$` and this
20
+ * server is meant to work with any MCP client, so the conservative alphabet is the one that
21
+ * cannot be wrong somewhere else.
22
+ */
23
+ export function toolNameFor(app, capabilityName) {
24
+ const qualified = capabilityName.startsWith(`${app}.`) ? capabilityName : `${app}.${capabilityName}`;
25
+ return qualified.replace(/[^A-Za-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 64);
26
+ }
27
+ /** Descriptors whose publishing process still exists. Cheap, and never the whole answer. */
28
+ export function candidateDescriptors(options = {}) {
29
+ const isAlive = options.isAlive ?? processIsAlive;
30
+ const candidates = [];
31
+ const dead = [];
32
+ for (const descriptor of readDescriptors(options.dir)) {
33
+ if (isAlive(descriptor.pid))
34
+ candidates.push(descriptor);
35
+ else
36
+ dead.push({ app: descriptor.app, reason: `not running (stale descriptor, pid ${descriptor.pid})` });
37
+ }
38
+ return { candidates, dead };
39
+ }
40
+ /** Build the live catalogue. Apps are probed in parallel; one being down never blocks another. */
41
+ export async function discover(options = {}) {
42
+ const { candidates, dead } = candidateDescriptors(options);
43
+ const entries = [];
44
+ const unreachable = [...dead];
45
+ const probes = candidates.map(async (descriptor) => {
46
+ const client = new CapabilityClient(descriptor, {
47
+ fetchImpl: options.fetchImpl,
48
+ timeoutMs: options.timeoutMs,
49
+ });
50
+ try {
51
+ const capabilities = await client.listCapabilities();
52
+ return { descriptor, capabilities };
53
+ }
54
+ catch (error) {
55
+ const reason = error instanceof CapabilityUnavailable ? error.message : `unreachable: ${error.message}`;
56
+ unreachable.push({ app: descriptor.app, reason });
57
+ return undefined;
58
+ }
59
+ });
60
+ for (const probe of await Promise.all(probes)) {
61
+ if (!probe)
62
+ continue;
63
+ for (const capability of probe.capabilities) {
64
+ entries.push({
65
+ app: probe.descriptor.app,
66
+ capability,
67
+ toolName: toolNameFor(probe.descriptor.app, capability.name),
68
+ });
69
+ }
70
+ }
71
+ entries.sort((a, b) => a.toolName.localeCompare(b.toolName));
72
+ unreachable.sort((a, b) => a.app.localeCompare(b.app));
73
+ return { entries, unreachable };
74
+ }
75
+ /** The description a model sees. It has to carry the app and the risk, not just the verb. */
76
+ export function describeEntry(entry) {
77
+ const risk = entry.capability.reversibility && entry.capability.reversibility !== 'safe'
78
+ ? ` [${entry.capability.reversibility}]`
79
+ : '';
80
+ return `[${entry.app}]${risk} ${entry.capability.description}`;
81
+ }
82
+ //# sourceMappingURL=catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogue.js","sourceRoot":"","sources":["../src/catalogue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Finding the XENO applications that are running right now.
3
+ *
4
+ * Every XENO app that opens a capability server publishes `{ url, token, pid }` to
5
+ * `~/.xeno/capability-servers/<app>.json` (`XENO AGENT CAPABILITY - SPEC.md` §10.1). That
6
+ * directory is discovery, and it is the ONLY trustworthy source for where an app is: the
7
+ * documented port block in `XENO PORT ALLOCATIONS.md` is a *preference*, and an app that
8
+ * loses its preferred port binds another and publishes the one it actually got. Measured
9
+ * 2026-09-08: Canvas's descriptor said 7395 against a documented 7333, and five of eight
10
+ * apps sat outside the documented block entirely.
11
+ *
12
+ * 🔴 **A descriptor is not a running server.** The file outlives the process that wrote it,
13
+ * so the STEADY STATE of that directory is stale, not live — on 2026-09-08 all eight
14
+ * descriptors named dead pids. Anything that reads a descriptor and concludes the app is up
15
+ * will offer an agent tools that cannot run, and the agent will report the app broken.
16
+ * {@link liveDescriptors} exists so that mistake is not available.
17
+ */
18
+ /** Where every XENO app publishes itself. */
19
+ export declare const CAPABILITY_SERVER_DIR: string;
20
+ export interface CapabilityDescriptor {
21
+ /** The app's slug, taken from the FILE NAME — `canvas.json` is `canvas`. */
22
+ app: string;
23
+ url: string;
24
+ token: string;
25
+ pid?: number | undefined;
26
+ path: string;
27
+ }
28
+ /** Read every descriptor on disk. Says nothing about whether any of them is alive. */
29
+ export declare function readDescriptors(dir?: string): CapabilityDescriptor[];
30
+ /**
31
+ * Is the process that published this descriptor still alive?
32
+ *
33
+ * `kill(pid, 0)` sends no signal; it asks whether the pid can be signalled. **`EPERM` means
34
+ * the process EXISTS** and belongs to someone else, so treating any throw as "dead" would
35
+ * report a running app as gone. A descriptor with no pid is not evidence of death either —
36
+ * it is absence of evidence, and the HTTP probe decides.
37
+ */
38
+ export declare function processIsAlive(pid: number | undefined): boolean;
39
+ //# sourceMappingURL=descriptors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptors.d.ts","sourceRoot":"","sources":["../src/descriptors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,6CAA6C;AAC7C,eAAO,MAAM,qBAAqB,QAAiD,CAAA;AAEnF,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAA8B,GAAG,oBAAoB,EAAE,CA8B3F;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQ/D"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Finding the XENO applications that are running right now.
3
+ *
4
+ * Every XENO app that opens a capability server publishes `{ url, token, pid }` to
5
+ * `~/.xeno/capability-servers/<app>.json` (`XENO AGENT CAPABILITY - SPEC.md` §10.1). That
6
+ * directory is discovery, and it is the ONLY trustworthy source for where an app is: the
7
+ * documented port block in `XENO PORT ALLOCATIONS.md` is a *preference*, and an app that
8
+ * loses its preferred port binds another and publishes the one it actually got. Measured
9
+ * 2026-09-08: Canvas's descriptor said 7395 against a documented 7333, and five of eight
10
+ * apps sat outside the documented block entirely.
11
+ *
12
+ * 🔴 **A descriptor is not a running server.** The file outlives the process that wrote it,
13
+ * so the STEADY STATE of that directory is stale, not live — on 2026-09-08 all eight
14
+ * descriptors named dead pids. Anything that reads a descriptor and concludes the app is up
15
+ * will offer an agent tools that cannot run, and the agent will report the app broken.
16
+ * {@link liveDescriptors} exists so that mistake is not available.
17
+ */
18
+ import { readdirSync, readFileSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ /** Where every XENO app publishes itself. */
22
+ export const CAPABILITY_SERVER_DIR = join(homedir(), '.xeno', 'capability-servers');
23
+ /** Read every descriptor on disk. Says nothing about whether any of them is alive. */
24
+ export function readDescriptors(dir = CAPABILITY_SERVER_DIR) {
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(dir);
28
+ }
29
+ catch {
30
+ // No directory means no XENO app has ever opened a capability port here. That is a
31
+ // legitimate empty answer, not a failure to report.
32
+ return [];
33
+ }
34
+ const descriptors = [];
35
+ for (const entry of entries) {
36
+ if (!entry.endsWith('.json'))
37
+ continue;
38
+ const path = join(dir, entry);
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
41
+ if (typeof parsed.url !== 'string' || typeof parsed.token !== 'string')
42
+ continue;
43
+ descriptors.push({
44
+ app: entry.slice(0, -'.json'.length),
45
+ url: parsed.url.replace(/\/+$/, ''),
46
+ token: parsed.token,
47
+ pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
48
+ path,
49
+ });
50
+ }
51
+ catch {
52
+ // A half-written or corrupt descriptor is one app being unreachable, never a reason
53
+ // to hide the others.
54
+ continue;
55
+ }
56
+ }
57
+ return descriptors.sort((a, b) => a.app.localeCompare(b.app));
58
+ }
59
+ /**
60
+ * Is the process that published this descriptor still alive?
61
+ *
62
+ * `kill(pid, 0)` sends no signal; it asks whether the pid can be signalled. **`EPERM` means
63
+ * the process EXISTS** and belongs to someone else, so treating any throw as "dead" would
64
+ * report a running app as gone. A descriptor with no pid is not evidence of death either —
65
+ * it is absence of evidence, and the HTTP probe decides.
66
+ */
67
+ export function processIsAlive(pid) {
68
+ if (pid === undefined)
69
+ return true;
70
+ try {
71
+ process.kill(pid, 0);
72
+ return true;
73
+ }
74
+ catch (error) {
75
+ return error.code === 'EPERM';
76
+ }
77
+ }
78
+ //# sourceMappingURL=descriptors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptors.js","sourceRoot":"","sources":["../src/descriptors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAA;AAWnF,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,MAAc,qBAAqB;IACjE,IAAI,OAAiB,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,mFAAmF;QACnF,oDAAoD;QACpD,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,WAAW,GAA2B,EAAE,CAAA;IAC9C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAQ;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAkC,CAAA;YACtF,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;gBAAE,SAAQ;YAChF,WAAW,CAAC,IAAI,CAAC;gBACf,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,GAAG,EAAE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;gBAC5D,IAAI;aACL,CAAC,CAAA;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,oFAAoF;YACpF,sBAAsB;YACtB,SAAQ;QACV,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAuB;IACpD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAClC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAA;IAC1D,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './descriptors.js';
2
+ export * from './capabilityClient.js';
3
+ export * from './catalogue.js';
4
+ export * from './mcpServer.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './descriptors.js';
2
+ export * from './capabilityClient.js';
3
+ export * from './catalogue.js';
4
+ export * from './mcpServer.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * An MCP server over stdio that serves every running XENO application's capabilities.
3
+ *
4
+ * ## Why stdio and why no MCP SDK
5
+ *
6
+ * `PluginMcpManagerService` supports `stdio` and `sse`, and stdio is the right one here: the
7
+ * adapter must hold each app's bearer token, and a stdio child of the privileged host holds
8
+ * it in a process the renderer cannot reach. An `sse` server would be a second local HTTP
9
+ * port with its own auth story, to reach local HTTP ports that already have one.
10
+ *
11
+ * The wire protocol is JSON-RPC 2.0, newline-delimited, and the three methods a tool server
12
+ * needs are `initialize`, `tools/list` and `tools/call`. That is small enough that adding a
13
+ * dependency to get it would cost more than it saves — and this package is spawned by a
14
+ * signed host, so its dependency graph is part of what has to be trusted.
15
+ *
16
+ * 🔴 **`tools/list` reports `inputSchema` as a COMPLETE JSON Schema**, per the MCP spec. The
17
+ * consuming SDK used to read it as a bare properties map and corrupt every tool's parameters
18
+ * (fixed in `xeno-agent-sdk` `src/mcp/tool-adapter.ts`). Do not "fix" this end to match an
19
+ * old client: a spec-shaped payload is the thing that is correct for every other client too.
20
+ */
21
+ import { Readable, Writable } from 'node:stream';
22
+ import { type Catalogue, type DiscoverOptions } from './catalogue.js';
23
+ export declare const PROTOCOL_VERSION = "2024-11-05";
24
+ export declare const SERVER_NAME = "xeno-capabilities";
25
+ interface JsonRpcRequest {
26
+ jsonrpc: '2.0';
27
+ id?: string | number | null;
28
+ method: string;
29
+ params?: Record<string, unknown>;
30
+ }
31
+ type JsonRpcResponse = {
32
+ jsonrpc: '2.0';
33
+ id: string | number | null;
34
+ result: unknown;
35
+ } | {
36
+ jsonrpc: '2.0';
37
+ id: string | number | null;
38
+ error: {
39
+ code: number;
40
+ message: string;
41
+ };
42
+ };
43
+ export interface McpServerOptions extends DiscoverOptions {
44
+ /** Injected so the whole request/response cycle can be exercised without a real app. */
45
+ discoverImpl?: ((options: DiscoverOptions) => Promise<Catalogue>) | undefined;
46
+ }
47
+ /**
48
+ * Handle one JSON-RPC request.
49
+ *
50
+ * Exported because this is where the protocol behaviour lives; a test that drives it through
51
+ * two pipes proves the plumbing, and a test that calls this proves the answers.
52
+ */
53
+ export declare function handleRequest(request: JsonRpcRequest, options?: McpServerOptions): Promise<JsonRpcResponse | undefined>;
54
+ /** Run the server against a pair of streams until the input ends. */
55
+ export declare function serve(input?: Readable, output?: Writable, options?: McpServerOptions): Promise<void>;
56
+ export {};
57
+ //# sourceMappingURL=mcpServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpServer.d.ts","sourceRoot":"","sources":["../src/mcpServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAIhD,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAE9F,eAAO,MAAM,gBAAgB,eAAe,CAAA;AAC5C,eAAO,MAAM,WAAW,sBAAsB,CAAA;AAE9C,UAAU,cAAc;IACtB,OAAO,EAAE,KAAK,CAAA;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,KAAK,eAAe,GAChB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/D;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAE5F,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,wFAAwF;IACxF,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,SAAS,CAAA;CAC9E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAgHtC;AAED,qEAAqE;AACrE,wBAAgB,KAAK,CACnB,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,EACjC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * An MCP server over stdio that serves every running XENO application's capabilities.
3
+ *
4
+ * ## Why stdio and why no MCP SDK
5
+ *
6
+ * `PluginMcpManagerService` supports `stdio` and `sse`, and stdio is the right one here: the
7
+ * adapter must hold each app's bearer token, and a stdio child of the privileged host holds
8
+ * it in a process the renderer cannot reach. An `sse` server would be a second local HTTP
9
+ * port with its own auth story, to reach local HTTP ports that already have one.
10
+ *
11
+ * The wire protocol is JSON-RPC 2.0, newline-delimited, and the three methods a tool server
12
+ * needs are `initialize`, `tools/list` and `tools/call`. That is small enough that adding a
13
+ * dependency to get it would cost more than it saves — and this package is spawned by a
14
+ * signed host, so its dependency graph is part of what has to be trusted.
15
+ *
16
+ * 🔴 **`tools/list` reports `inputSchema` as a COMPLETE JSON Schema**, per the MCP spec. The
17
+ * consuming SDK used to read it as a bare properties map and corrupt every tool's parameters
18
+ * (fixed in `xeno-agent-sdk` `src/mcp/tool-adapter.ts`). Do not "fix" this end to match an
19
+ * old client: a spec-shaped payload is the thing that is correct for every other client too.
20
+ */
21
+ import { createInterface } from 'node:readline';
22
+ import { Readable, Writable } from 'node:stream';
23
+ import { CapabilityClient, CapabilityUnavailable } from './capabilityClient.js';
24
+ import { readDescriptors } from './descriptors.js';
25
+ import { describeEntry, discover } from './catalogue.js';
26
+ export const PROTOCOL_VERSION = '2024-11-05';
27
+ export const SERVER_NAME = 'xeno-capabilities';
28
+ /**
29
+ * Handle one JSON-RPC request.
30
+ *
31
+ * Exported because this is where the protocol behaviour lives; a test that drives it through
32
+ * two pipes proves the plumbing, and a test that calls this proves the answers.
33
+ */
34
+ export async function handleRequest(request, options = {}) {
35
+ const id = request.id ?? null;
36
+ // A notification (no id) gets no reply, ever. Replying to `notifications/initialized` with
37
+ // an id of null is a protocol violation some clients treat as fatal.
38
+ const isNotification = request.id === undefined;
39
+ switch (request.method) {
40
+ case 'initialize':
41
+ return {
42
+ jsonrpc: '2.0',
43
+ id,
44
+ result: {
45
+ protocolVersion: PROTOCOL_VERSION,
46
+ capabilities: { tools: { listChanged: false } },
47
+ serverInfo: { name: SERVER_NAME, version: '0.1.30' },
48
+ instructions: 'Tools here drive XENO applications that are running on this machine right now. '
49
+ + 'A tool named <app>_<domain>_<verb> acts on that application. If an application is '
50
+ + 'not running its tools are absent, not failing — ask the user to open it.',
51
+ },
52
+ };
53
+ case 'ping':
54
+ return { jsonrpc: '2.0', id, result: {} };
55
+ case 'tools/list': {
56
+ const catalogue = await (options.discoverImpl ?? discover)(options);
57
+ return {
58
+ jsonrpc: '2.0',
59
+ id,
60
+ result: {
61
+ tools: catalogue.entries.map((entry) => ({
62
+ name: entry.toolName,
63
+ description: describeEntry(entry),
64
+ inputSchema: entry.capability.schema,
65
+ })),
66
+ },
67
+ };
68
+ }
69
+ case 'tools/call': {
70
+ const name = typeof request.params?.['name'] === 'string' ? request.params['name'] : '';
71
+ const args = request.params?.['arguments'] && typeof request.params['arguments'] === 'object'
72
+ ? request.params['arguments']
73
+ : {};
74
+ const catalogue = await (options.discoverImpl ?? discover)(options);
75
+ const entry = catalogue.entries.find((candidate) => candidate.toolName === name);
76
+ if (!entry) {
77
+ /*
78
+ * A refusal has to say WHICH failure this is. "No such tool" and "the app that owns
79
+ * that tool is closed" lead to opposite next actions, and an agent told only the
80
+ * first will conclude the capability does not exist and stop trying.
81
+ */
82
+ const app = name.split('_')[0] ?? '';
83
+ const down = catalogue.unreachable.find((candidate) => candidate.app === app);
84
+ const message = down
85
+ ? `${app} is not available: ${down.reason}. Ask the user to open ${app}, then retry.`
86
+ : `No tool named ${name}. Running applications: ${[...new Set(catalogue.entries.map((candidate) => candidate.app))].join(', ') || 'none'}.`;
87
+ return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: message }] } };
88
+ }
89
+ const descriptor = readDescriptors(options.dir).find((candidate) => candidate.app === entry.app);
90
+ if (!descriptor) {
91
+ return {
92
+ jsonrpc: '2.0',
93
+ id,
94
+ result: {
95
+ isError: true,
96
+ content: [{ type: 'text', text: `${entry.app} stopped publishing itself between listing and this call.` }],
97
+ },
98
+ };
99
+ }
100
+ const client = new CapabilityClient(descriptor, {
101
+ fetchImpl: options.fetchImpl,
102
+ timeoutMs: options.timeoutMs,
103
+ });
104
+ try {
105
+ const result = await client.invoke(entry.capability.name, args);
106
+ /*
107
+ * `isError` is derived from the application's own §8 envelope rather than assumed
108
+ * false. A refusal returned as a success is the fabricated-success shape this
109
+ * ecosystem keeps shipping, and here it would be invisible: the model would read a
110
+ * failure body as a result and carry on.
111
+ */
112
+ const envelope = result;
113
+ const failed = envelope?.ok === false || envelope?.success === false || envelope?.isError === true;
114
+ return {
115
+ jsonrpc: '2.0',
116
+ id,
117
+ result: {
118
+ ...(failed ? { isError: true } : {}),
119
+ content: [{ type: 'text', text: JSON.stringify(result) }],
120
+ },
121
+ };
122
+ }
123
+ catch (error) {
124
+ const message = error instanceof CapabilityUnavailable
125
+ ? error.message
126
+ : `${entry.app} failed: ${error.message}`;
127
+ return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: message }] } };
128
+ }
129
+ }
130
+ default:
131
+ if (isNotification)
132
+ return undefined;
133
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${request.method}` } };
134
+ }
135
+ }
136
+ /** Run the server against a pair of streams until the input ends. */
137
+ export function serve(input = process.stdin, output = process.stdout, options = {}) {
138
+ const lines = createInterface({ input, crlfDelay: Infinity });
139
+ /*
140
+ * Requests are answered in order. MCP allows concurrency, but these tools drive a user's
141
+ * open document: two overlapping edits arriving in an order nobody chose is worse than one
142
+ * being slightly slower, and the applications themselves serialise anyway.
143
+ */
144
+ let queue = Promise.resolve();
145
+ lines.on('line', (line) => {
146
+ const text = line.trim();
147
+ if (!text)
148
+ return;
149
+ queue = queue.then(async () => {
150
+ let request;
151
+ try {
152
+ request = JSON.parse(text);
153
+ }
154
+ catch {
155
+ output.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })}\n`);
156
+ return;
157
+ }
158
+ try {
159
+ const response = await handleRequest(request, options);
160
+ if (response)
161
+ output.write(`${JSON.stringify(response)}\n`);
162
+ }
163
+ catch (error) {
164
+ // A throw escaping here would leave the client waiting on an id forever, which reads
165
+ // as a hang rather than a failure it can act on.
166
+ if (request.id !== undefined) {
167
+ output.write(`${JSON.stringify({
168
+ jsonrpc: '2.0',
169
+ id: request.id ?? null,
170
+ error: { code: -32603, message: error.message },
171
+ })}\n`);
172
+ }
173
+ }
174
+ });
175
+ });
176
+ return new Promise((resolve) => {
177
+ lines.on('close', () => {
178
+ queue.then(() => resolve()).catch(() => resolve());
179
+ });
180
+ });
181
+ }
182
+ //# sourceMappingURL=mcpServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpServer.js","sourceRoot":"","sources":["../src/mcpServer.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@xenosystem/capability-mcp",
3
+ "version": "0.1.31",
4
+ "license": "UNLICENSED",
5
+ "description": "Presents every running XENO application's capability server to an agent as MCP tools.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "xeno-capability-mcp": "./dist/bin.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./bin": {
18
+ "types": "./dist/bin.d.ts",
19
+ "import": "./dist/bin.js",
20
+ "default": "./dist/bin.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "scripts"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/XENO-CORPORATION/xeno-agent-interface.git",
33
+ "directory": "packages/capability-mcp"
34
+ },
35
+ "scripts": {
36
+ "prepack": "tsc -b",
37
+ "smoke:live": "node scripts/smoke-live-app.mjs"
38
+ }
39
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The gate that decides whether tool inheritance actually works.
3
+ *
4
+ * Unit tests over a fake capability server prove the translation and never the connection —
5
+ * "built, tested, unreachable" is the defect this ecosystem has recorded seven times. So this
6
+ * spawns the REAL adapter binary as a child process, speaks REAL newline-delimited JSON-RPC
7
+ * to its stdin, and asserts a REAL XENO application changed as a result.
8
+ *
9
+ * node packages/capability-mcp/scripts/smoke-live-app.mjs
10
+ *
11
+ * It requires a XENO application to be running with its capability server open, e.g.
12
+ * XENO_CANVAS_CAPABILITY_SERVER=1 electron . --capability-server
13
+ *
14
+ * 🔴 It REFUSES rather than passes when nothing is running. A smoke test that reports success
15
+ * because it found nothing to test is the shape that let four uninstallable releases ship.
16
+ */
17
+
18
+ import { spawn } from 'node:child_process'
19
+ import { createInterface } from 'node:readline'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const BIN = fileURLToPath(new URL('../dist/bin.js', import.meta.url))
23
+
24
+ const gates = []
25
+ function gate(name, ok, detail = '') {
26
+ gates.push({ name, ok, detail })
27
+ process.stdout.write(` ${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}\n`)
28
+ }
29
+
30
+ const child = spawn(process.execPath, [BIN], { stdio: ['pipe', 'pipe', 'inherit'] })
31
+ const lines = createInterface({ input: child.stdout })
32
+ const pending = new Map()
33
+ lines.on('line', (line) => {
34
+ if (!line.trim()) return
35
+ const message = JSON.parse(line)
36
+ const resolve = pending.get(message.id)
37
+ if (resolve) {
38
+ pending.delete(message.id)
39
+ resolve(message)
40
+ }
41
+ })
42
+
43
+ let nextId = 1
44
+ function call(method, params) {
45
+ const id = nextId++
46
+ return new Promise((resolve, reject) => {
47
+ pending.set(id, resolve)
48
+ // A hang is a failure, not a wait: without this the script blocks forever on a child
49
+ // that stopped answering, and a run that neither passes nor fails gets retried instead
50
+ // of investigated.
51
+ setTimeout(() => {
52
+ if (pending.delete(id)) reject(new Error(`${method} did not answer in 60s`))
53
+ }, 60_000)
54
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
55
+ })
56
+ }
57
+
58
+ const text = (response) => response.result?.content?.[0]?.text ?? ''
59
+
60
+ try {
61
+ const init = await call('initialize', { protocolVersion: '2024-11-05', capabilities: {} })
62
+ gate('the adapter completes an MCP handshake', Boolean(init.result?.protocolVersion), init.result?.serverInfo?.name)
63
+
64
+ const listed = await call('tools/list', {})
65
+ const tools = listed.result?.tools ?? []
66
+ const apps = [...new Set(tools.map((tool) => tool.name.split('_')[0]))]
67
+ gate('a running application is discovered', tools.length > 0, `${tools.length} tools from ${apps.join(', ') || 'nothing'}`)
68
+ if (tools.length === 0) {
69
+ console.error('\nNo XENO application is running with a capability server open.')
70
+ console.error('This is a REFUSAL, not a pass: nothing was proven.')
71
+ process.exit(2)
72
+ }
73
+
74
+ const withSchema = tools.find((tool) => Object.keys(tool.inputSchema?.properties ?? {}).length > 0)
75
+ gate(
76
+ 'a tool arrives with a COMPLETE JSON Schema, not a properties map',
77
+ withSchema?.inputSchema?.type === 'object' && !('properties' in (withSchema.inputSchema.properties ?? {})),
78
+ withSchema?.name,
79
+ )
80
+
81
+ const has = (name) => tools.some((tool) => tool.name === name)
82
+ gate('the application exposes what this proof needs', has('canvas_document_create') && has('canvas_node_create_rectangle') && has('canvas_node_find'))
83
+
84
+ // 🔴 The gate that matters: an agent-side tool call must CHANGE the running application.
85
+ await call('tools/call', { name: 'canvas_document_create', arguments: {} })
86
+ const label = `mcp-smoke-${Date.now()}`
87
+ const created = await call('tools/call', {
88
+ name: 'canvas_node_create_rectangle',
89
+ arguments: { x: 24, y: 24, width: 120, height: 80, name: label },
90
+ })
91
+ gate('a tool call is not an error', created.result?.isError !== true, text(created).slice(0, 160))
92
+
93
+ const found = await call('tools/call', { name: 'canvas_node_find', arguments: { name: label } })
94
+ const body = text(found)
95
+ gate('the application actually changed — the node is there when read back', body.includes(label), body.slice(0, 200))
96
+
97
+ /*
98
+ * The refusal half, against an operation the application GENUINELY rejects.
99
+ *
100
+ * The first version of this gate asked for a node by an id that does not exist — Canvas
101
+ * answers `{ok:true, data:[]}` to that, so the assertion could not discriminate and passed
102
+ * on a working and a broken adapter alike. `page.set_current` with an unknown page really
103
+ * does answer `ok:false`, which is what makes the check able to fail.
104
+ */
105
+ const refused = await call('tools/call', { name: 'canvas_page_set_current', arguments: { pageId: 'not-a-page' } })
106
+ gate(
107
+ "the application's own refusal arrives as isError, not as a result",
108
+ refused.result?.isError === true && /ok":false/.test(text(refused)),
109
+ text(refused).slice(0, 140),
110
+ )
111
+
112
+ const missing = await call('tools/call', { name: 'motion_timeline_play', arguments: {} })
113
+ gate(
114
+ 'a closed application is named as CLOSED, not as a missing tool',
115
+ /not available|No tool named/.test(text(missing)),
116
+ text(missing).slice(0, 140),
117
+ )
118
+ } finally {
119
+ child.stdin.end()
120
+ child.kill()
121
+ }
122
+
123
+ const failed = gates.filter((entry) => !entry.ok)
124
+ process.stdout.write(`\n${gates.length - failed.length}/${gates.length} gates passed\n`)
125
+ process.exit(failed.length ? 1 : 0)