@impetik/xeer-mcp 0.2.3 → 0.2.5

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 CHANGED
@@ -1,15 +1,22 @@
1
1
  # `@impetik/xeer-mcp`
2
2
 
3
3
  Model Context Protocol server for [Xeer](https://docs.xeer.run), the framework for building and shipping
4
- full-stack apps. It exposes the scaffold → check → test → build → deploy loop as MCP tools, each
5
- returning the `xeer.command.v0` JSON envelope the `xeer` CLI already prints.
4
+ full-stack apps. It exposes the scaffold → check → test → build → deploy loop as MCP tools with a
5
+ stable `xeer.mcp-result.v0` result protocol.
6
6
 
7
7
  📖 **Documentation: [docs.xeer.run](https://docs.xeer.run)** · **Building with AI agents:
8
8
  [docs.xeer.run/guides/agents](https://docs.xeer.run/guides/agents)** · **Diagnostics:
9
9
  [docs.xeer.run/reference/diagnostics](https://docs.xeer.run/reference/diagnostics)**
10
10
 
11
- The [repository](https://github.com/impetik/xeer) also ships the same protocol as a Claude Code skill
12
- under `.claude/skills/xeer/`, which you can copy into your own project.
11
+ The [repository](https://github.com/impetik/xeer) keeps the canonical portable skill under
12
+ `.agents/skills/xeer/` and generates provider projections from it. `xeer new` installs those files;
13
+ for an existing project run `xeer agent setup`.
14
+
15
+ Start with `xeer://project/context` when the client supports MCP resources. Separate normalized project
16
+ resources live at `xeer://project/manifest`, `xeer://project/operations`, and `xeer://project/tests`;
17
+ `xeer://docs/index` lists installed-version Markdown pages exposed by `xeer://docs/{path}`, and
18
+ `xeer://diagnostics/{code}` resolves a stable diagnostic definition. Clients without resource support
19
+ can call `xeer_agent_context` and `xeer_docs_search` instead.
13
20
 
14
21
  ## Run it
15
22
 
@@ -19,41 +26,99 @@ under `.claude/skills/xeer/`, which you can copy into your own project.
19
26
  "mcpServers": {
20
27
  "xeer": {
21
28
  "command": "npx",
22
- "args": ["--package=@impetik/xeer-mcp", "--", "xeer-mcp"],
29
+ "args": ["--package=@impetik/xeer-mcp@<XEER_VERSION>", "--", "xeer-mcp"],
23
30
  "env": { "XEER_MCP_ROOT": "." }
24
31
  }
25
32
  }
26
33
  }
27
34
  ```
28
35
 
36
+ Prefer `xeer agent setup --target mcp`: it replaces `<XEER_VERSION>` and writes this configuration with the exact MCP package
37
+ version compatible with the installed CLI, records a generated-file hash, and refuses to overwrite an
38
+ unowned `.mcp.json`. `XEER_MCP_ROOT: "."` keeps all project paths confined to the checkout.
39
+
40
+ This starts the default `author` profile: `xeer_deploy_preview` always targets preview, and no
41
+ production-changing tool is registered. An operator who deliberately wants to expose reviewed
42
+ promotion must add `"XEER_MCP_PROFILE": "operator"` to the server environment. A tool argument cannot
43
+ change profiles, and operator promotion requires the exact `receiptId` returned by preview deploy. MCP
44
+ has no artifact-id or current-slot bypass.
45
+
29
46
  From this repository, after `pnpm -r build`:
30
47
 
31
48
  ```sh
32
49
  node packages/mcp/dist/main.js # stdio; or `pnpm mcp` from the workspace root
33
50
  ```
34
51
 
35
- ## Tools
36
-
37
- | Tool | CLI it runs | Returns |
38
- | --- | --- | --- |
39
- | `xeer_check` | `xeer check <dir> --json` | Manifest + analysis diagnostics; `result.manifest` when clean |
40
- | `xeer_test` | `xeer test <dir> --json` | `{ ok, total, passed, failed, cases, diagnostics, events }` |
41
- | `xeer_build` | `xeer build <dir> --json` | `result.artifactId`, modules, assets, operations |
42
- | `xeer_new` | `xeer new <dir> [--template <name>] --json` | `result.template`, `result.files` |
43
- | `xeer_doctor` | `xeer doctor <dir> --json` | `result.checks`, `result.summary` |
44
- | `xeer_deploy` | `xeer deploy <dir> --json` | `result.url` (`environment: "preview"` deploys to the side-by-side preview URL) |
45
- | `xeer_promote` | `xeer promote <dir> --json` | `result.artifactId`, `result.replaced`, `result.url` |
46
- | `xeer_auth_status` | `xeer auth status --json` | Builder identity and credential expiry |
47
- | `xeer_inspect` | `xeer inspect`/`state`/`logs` | Inspector response for a running preview |
48
- | `xeer_dev_start` | `xeer dev <dir> --json --port 0` | Session handle, cursor, and the events up to `preview.ready` |
49
- | `xeer_dev_status` | — | `xeer.dev.v0` events after a cursor, plus `openDiagnostics` |
50
- | `xeer_dev_stop` | — | Final events; shuts down over `xeer.dev.control.v0` IPC |
51
- | `xeer_diagnostics` | — | What an `XE####` code means and the edit that closes it |
52
-
53
- Every command tool returns the envelope both as `structuredContent` and as a JSON text
54
- block, so a client that ignores structured output still sees all of it. A tool result is
55
- always an envelope: when the CLI crashes without printing one, the server synthesizes
56
- `XE0000` rather than failing the call in a different shape.
52
+ ## Registered action policy
53
+
54
+ The generated registry is the complete MCP tool and exclusion surface:
55
+
56
+ <!-- xeer-action-reference:start -->
57
+ <!-- Generated from packages/spec/src/actions.ts by `pnpm generate:agent-reference`. Do not edit. -->
58
+
59
+ **MCP tools (15 total; 14 author, 15 operator).** Inputs ending in `?` are optional.
60
+
61
+ | Tool | Action | Profiles | Summary | Inputs | Effects | Safety | Path policy | Output | Human prerequisite |
62
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
63
+ | `xeer_check` | `check` | `author` / `operator` | Validate a project and report structured diagnostics. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
64
+ | `xeer_build` | `build` | `author` / `operator` | Build and verify a content-addressed application artifact. | `directory?`: `string` | `read-source`<br>`write-generated`<br>`run-local` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
65
+ | `xeer_test` | `test` | `author` / `operator` | Run application tests against fresh isolated local state. | `directory?`: `string`<br>`timeoutMilliseconds?`: `integer` [1000..1800000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
66
+ | `xeer_new` | `new` | `author` / `operator` | Create a new project from a supported scaffold. | `directory`: `string`<br>`template?`: `notes` / `todo` / `blog` / `personal-site` | `write-source` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
67
+ | `xeer_agent_context` | `agent.context` | `author` / `operator` | Read normalized project facts, operations, diagnostics, tests, and safe next actions. | `directory?`: `string` | `read-source` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.agent-context.v0` | none |
68
+ | `xeer_docs_search` | `docs.search` | `author` / `operator` | Search the installed-version Xeer documentation index. | `query`: `string`<br>`limit?`: `integer` [1..20]; default `5` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.docs-search.v0` | none |
69
+ | `xeer_doctor` | `doctor` | `author` / `operator` | Diagnose the toolchain and generated project state. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
70
+ | `xeer_deploy_preview` | `deploy.preview` | `author` / `operator` | Build and deploy an artifact to preview only. | `directory?`: `string`<br>`controlUrl?`: `string` | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | A human must establish the builder credential with xeer auth login. |
71
+ | `xeer_promote` | `promote.operator` | `operator` | Promote an exact review receipt in operator mode. | `directory?`: `string`<br>`receiptId`: `string`<br>`controlUrl?`: `string` | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | A human must review the exact artifact and start xeer-mcp with XEER_MCP_PROFILE=operator. |
72
+ | `xeer_auth_status` | `auth.status` | `author` / `operator` | Report builder credential metadata. | `controlUrl?`: `string` | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | none |
73
+ | `xeer_inspect` | `inspect` | `author` / `operator` | Read a running application manifest. | `previewUrl`: `string`<br>`view?`: `manifest` / `state` / `logs` / `export`; default `manifest`<br>`after?`: `string` | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | none |
74
+ | `xeer_dev_start` | `dev.start` | `author` / `operator` | Start a local development session. | `directory?`: `string`<br>`host?`: `string`<br>`port?`: `integer` [0..65535]<br>`timeoutMilliseconds?`: `integer` [1000..600000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
75
+ | `xeer_dev_status` | `dev.status` | `author` / `operator` | Read new events from a local development session. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0`<br>`waitMilliseconds?`: `integer` [0..600000]; default `0`<br>`limit?`: `integer` [1..1000]; default `200` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
76
+ | `xeer_dev_stop` | `dev.stop` | `author` / `operator` | Stop a local development session and release its lease. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0` | `run-local` | writes; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
77
+ | `xeer_diagnostics` | `diagnostics` | `author` / `operator` | Read the generated diagnostic catalogue. | `code?`: `string` | none | read-only; idempotent; reversible; non-destructive | `none` | `unversioned` | none |
78
+
79
+ **CLI actions intentionally excluded from MCP (31).**
80
+
81
+ | CLI action | Action | Summary | Effects | Safety | Path policy | Output | Why no MCP tool |
82
+ | --- | --- | --- | --- | --- | --- | --- | --- |
83
+ | `xeer agent setup` | `agent.setup` | Install or verify project-confined agent adapters. | `read-source`<br>`write-source` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
84
+ | `xeer deploy` | `deploy` | Build and deploy an application artifact. | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | MCP exposes a separate preview-only deploy action; direct production deploy stays CLI-only. |
85
+ | `xeer promote` | `promote` | Promote a preview artifact to production. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | MCP promotion is a separate action available only when the server starts in operator profile. |
86
+ | `xeer link` | `link` | Link a checkout to an application the builder owns. | `read-source`<br>`write-source`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
87
+ | `xeer deployments` | `deployments` | List deployment history for an application. | `network-read`<br>`read-state`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
88
+ | `xeer rollback` | `rollback` | Redeploy a previously deployed artifact. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
89
+ | `xeer disable` | `disable` | Take an application offline without deleting its data. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
90
+ | `xeer enable` | `enable` | Restore a disabled application to service. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
91
+ | `xeer delete` | `delete` | Permanently delete an application and all of its data. | `network-read`<br>`network-write`<br>`write-state`<br>`production-change` | writes; non-idempotent; irreversible; destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
92
+ | `xeer domains add` | `domains.add` | Attach a customer-owned hostname to an application. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
93
+ | `xeer domains ls` | `domains.ls` | List an application's platform and custom domains. | `network-read` | read-only; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
94
+ | `xeer domains status` | `domains.status` | Refresh hostname and certificate validation. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
95
+ | `xeer domains remove` | `domains.remove` | Detach a custom hostname and delete its provider certificate. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
96
+ | `xeer preview` | `preview` | Run a verified artifact in a local preview server. | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | Not exposed through MCP v0; use the CLI deliberately. |
97
+ | `xeer state` | `state.read` | Read record counts from a running application. | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
98
+ | `xeer state reset` | `state.reset` | Permanently reset local environment state. | `read-source`<br>`write-state` | writes; non-idempotent; irreversible; destructive | `project-relative` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
99
+ | `xeer logs` | `logs` | Read the bounded log ring of a running application. | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
100
+ | `xeer export` | `export` | Export application state to a document. | `network-read`<br>`read-state`<br>`write-file` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
101
+ | `xeer import` | `import` | Replace local or preview state from an export document. | `read-source`<br>`network-write`<br>`write-state` | writes; non-idempotent; irreversible; destructive | `project-or-url` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
102
+ | `xeer auth login` | `auth.login` | Establish a human builder credential. | `network-read`<br>`network-write`<br>`secret-write` | writes; non-idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
103
+ | `xeer auth logout` | `auth.logout` | Remove the local builder credential. | `network-write`<br>`secret-write` | writes; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
104
+ | `xeer auth as` | `auth.as` | Select a deterministic local development persona. | `write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
105
+ | `xeer auth clear` | `auth.clear` | Reset the local development persona. | `write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
106
+ | `xeer env set` | `env.set` | Set an environment value or secret. | `network-write`<br>`write-state`<br>`secret-write` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
107
+ | `xeer env ls` | `env.ls` | List environment variable names and metadata. | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
108
+ | `xeer env rm` | `env.rm` | Remove an environment value or secret. | `network-write`<br>`write-state`<br>`secret-write` | writes; idempotent; irreversible; destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
109
+ | `xeer env pull` | `env.pull` | Write development secrets to a local file. | `network-read`<br>`write-file`<br>`secret-read` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
110
+ | `xeer token create` | `token.create` | Issue a service builder token. | `network-write`<br>`secret-write` | writes; non-idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
111
+ | `xeer token ls` | `token.ls` | List service token metadata. | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
112
+ | `xeer token revoke` | `token.revoke` | Revoke a service builder token. | `network-write`<br>`secret-write` | writes; idempotent; irreversible; destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
113
+ | `xeer actions` | `actions` | Print the machine-readable action and safety catalogue. | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
114
+ <!-- xeer-action-reference:end -->
115
+
116
+ Every tool returns `{ protocol: "xeer.mcp-result.v0", action, ok, result?, error? }` both as
117
+ `structuredContent` and as a JSON text block. Command tools preserve the CLI's
118
+ `xeer.command.v0` envelope at `result.envelope`, alongside `result.exitCode`. Host argv,
119
+ raw stderr, and absolute directories are omitted. A CLI crash still produces an `XE0000`
120
+ command envelope; an application-level refusal produces a stable `error.code` without
121
+ turning the response into an MCP transport error.
57
122
 
58
123
  `xeer_test` is the exception, because `xeer test --json` streams `xeer.dev.v0` events rather
59
124
  than printing an envelope. It collects the run and returns the summary plus the unabridged
@@ -62,11 +127,6 @@ assertion, `XE1905` throw or unexpectedly refused call, `XE1906` timeout) that i
62
127
  matcher, expected and actual values, and the project-relative test location. It needs no
63
128
  session handle: unlike `dev`, a test run is short-lived and terminal.
64
129
 
65
- `xeer link` and `xeer env` are deliberately absent. Which hosted app a checkout deploys to
66
- is the human owner's decision, so on `XE5111`/`XE5120`/`XE5121` the surface reports the
67
- diagnostic and its documented repair instead of performing it. `xeer env` reads and writes
68
- secrets — `xeer env pull` returns plaintext — so it is not reachable through a tool call.
69
-
70
130
  ## Why dev is three tools
71
131
 
72
132
  An MCP tool call is request/response; `xeer dev` is a long-lived JSONL stream. The session
@@ -74,10 +134,10 @@ is therefore owned by the server and addressed by a `sessionId`, with the protoc
74
134
  `seq` as a resumable cursor:
75
135
 
76
136
  ```text
77
- xeer_dev_start → { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events }
137
+ xeer_dev_start → { result: { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events } }
78
138
  edit a file
79
- xeer_dev_status → { events: [compile.start, compile.diagnostic?, compile.ready?], openDiagnostics, generation }
80
- xeer_dev_stop → { status: 'stopped' }
139
+ xeer_dev_status → { result: { events: [compile.start, compile.diagnostic?, compile.ready?], openDiagnostics, generation } }
140
+ xeer_dev_stop → { result: { status: 'stopped' } }
81
141
  ```
82
142
 
83
143
  No event is summarized away, so the agent reads the same stream a terminal would show.
@@ -88,8 +148,18 @@ session exists for when the preview, the inspector, or rebuild/rollback behaviou
88
148
 
89
149
  | Variable | Effect |
90
150
  | --- | --- |
151
+ | `XEER_MCP_PROFILE` | `author` (default) exposes no production-changing action. `operator` additionally exposes exact-artifact `xeer_promote`; any other value refuses server startup. |
91
152
  | `XEER_MCP_ROOT` | Confinement root for every `directory` argument. Defaults to the server's working directory; paths that escape it are refused. |
92
153
  | `XEER_CLI` | Absolute path to `dist/cli.js`, overriding resolution through this package's dependencies. |
154
+ | `XEER_MCP_CONTROL_ORIGINS` | Comma-separated additional exact HTTPS or loopback development control-plane origins that MCP may send builder credentials to. Only hosted `https://control.xeer.run` is allowed by default; plaintext non-loopback origins are always refused. |
155
+ | `XEER_MCP_INSPECTOR_ORIGINS` | Comma-separated exact loopback HTTP origins for preview servers started outside this MCP server. A preview started with `xeer_dev_start` is allowed automatically only while that session remains active. |
156
+
157
+ URL-shaped inspector targets are confined separately: MCP accepts the loopback HTTP origin of an active
158
+ `xeer_dev_start` session (or an exact operator-configured origin) and exact hosted
159
+ `https://<app>.xeer.run` origins. Bare application names and appIds are safe because the authenticated
160
+ control plane resolves them; arbitrary, private-network,
161
+ link-local, custom-domain, and credential-bearing URLs are refused before the CLI runs. Use an app
162
+ name or appId to inspect an application that serves a custom domain.
93
163
 
94
164
  Always stop dev sessions you start: local state is leased per project and mode, so a leaked
95
165
  session makes the next run fail with `XE1812`. The stdio entrypoint stops every session on
@@ -1,5 +1,12 @@
1
1
  import { fork } from 'node:child_process';
2
2
  import { resolveXeerCli } from './xeer-cli.js';
3
+ function hasPreviewUrls(value) {
4
+ if (typeof value !== 'object' || value === null)
5
+ return false;
6
+ const candidate = value;
7
+ return ['url', 'healthUrl', 'inspectorUrl', 'debugUrl', 'logsUrl']
8
+ .every((key) => typeof candidate[key] === 'string');
9
+ }
3
10
  const MAX_BUFFERED_EVENTS = 2_000;
4
11
  const MAX_STDERR_CHARACTERS = 4_096;
5
12
  const DEFAULT_START_TIMEOUT_MILLISECONDS = 180_000;
@@ -123,6 +130,11 @@ export class DevSession {
123
130
  this.#compiling = true;
124
131
  break;
125
132
  case 'server.restarted':
133
+ // A Vite restart may bind a different port when automation requested port 0. The URLs on
134
+ // a current event replace preview.ready immediately. Keep the last valid discovery values
135
+ // when supervising an older CLI whose pre-v0 payload did not yet carry URLs.
136
+ if (hasPreviewUrls(event.data))
137
+ this.#preview = event.data;
126
138
  this.#restarting = false;
127
139
  this.#compiling = false;
128
140
  break;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createXeerMcpServer, type XeerMcpServer } from './server.js';
1
+ export { createXeerMcpServer, mcpProfile, type XeerMcpServer, type XeerMcpServerOptions, } from './server.js';
2
2
  export { DevSession, DevSessionRegistry, type DevSessionOptions, type DevSessionRead, type DevSessionStatus, type DevSessionSummary, type PreviewUrls, } from './dev-session.js';
3
3
  export { runXeerTests, type RunTestsOptions, type TestCaseResult, type TestFailure, type TestRunResult, } from './test-run.js';
4
4
  export { projectRoot, resolveDirectory, resolveXeerCli, runXeerCommand, XeerCliError, type CommandEnvelope, type CommandRun, type RunOptions, } from './xeer-cli.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { createXeerMcpServer } from './server.js';
1
+ export { createXeerMcpServer, mcpProfile, } from './server.js';
2
2
  export { DevSession, DevSessionRegistry, } from './dev-session.js';
3
3
  export { runXeerTests, } from './test-run.js';
4
4
  export { projectRoot, resolveDirectory, resolveXeerCli, runXeerCommand, XeerCliError, } from './xeer-cli.js';
package/dist/main.js CHANGED
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { createXeerMcpServer } from './server.js';
3
+ import { createXeerMcpServer, mcpProfile } from './server.js';
4
4
  /**
5
5
  * stdio entrypoint. Nothing may be written to stdout except JSON-RPC frames, so
6
6
  * every diagnostic message from this process goes to stderr.
7
7
  */
8
- const { server, devSessions } = createXeerMcpServer();
8
+ let instance;
9
+ try {
10
+ instance = createXeerMcpServer({ profile: mcpProfile(process.env.XEER_MCP_PROFILE) });
11
+ }
12
+ catch (error) {
13
+ process.stderr.write(`xeer-mcp failed to start: ${error instanceof Error ? error.message : String(error)}\n`);
14
+ process.exit(1);
15
+ }
16
+ const { server, devSessions } = instance;
9
17
  let closing = false;
10
18
  async function shutdown(code) {
11
19
  if (closing)
@@ -0,0 +1,4 @@
1
+ /** Undefined means the explicit control origin is safe to receive the builder credential. */
2
+ export declare function controlOriginRefusal(value: string | undefined, configured?: string | undefined): string | undefined;
3
+ /** Undefined means the target cannot make MCP perform an arbitrary direct network read. */
4
+ export declare function inspectorTargetRefusal(value: string, activeLocalOrigins?: readonly string[], configured?: string | undefined): string | undefined;
@@ -0,0 +1,65 @@
1
+ import { DEFAULT_XEER_CONTROL_ORIGIN, exactHttpOrigin, isHostedXeerApplicationTarget, isLocalInspectorTarget, isLoopbackHostname, } from '@impetik/xeer-spec/network-policy';
2
+ const APP_ID = /^app_[A-Za-z0-9_-]{8,96}$/u;
3
+ const APPLICATION_NAME = /^[a-z][a-z0-9-]{1,62}$/u;
4
+ function configuredControlOrigins(value) {
5
+ const result = new Set();
6
+ for (const entry of value?.split(',') ?? []) {
7
+ const origin = exactHttpOrigin(entry);
8
+ if (!origin)
9
+ continue;
10
+ const url = new URL(origin);
11
+ // Configuration can add an HTTPS control plane or a loopback development one; it can never opt
12
+ // a plaintext non-loopback endpoint into receiving a credential.
13
+ if (url.protocol === 'https:' || isLoopbackHostname(url.hostname))
14
+ result.add(origin);
15
+ }
16
+ return result;
17
+ }
18
+ function configuredInspectorOrigins(value) {
19
+ const result = new Set();
20
+ for (const entry of value?.split(',') ?? []) {
21
+ const origin = exactHttpOrigin(entry);
22
+ if (!origin)
23
+ continue;
24
+ const url = new URL(origin);
25
+ if (url.protocol === 'http:' && isLoopbackHostname(url.hostname))
26
+ result.add(origin);
27
+ }
28
+ return result;
29
+ }
30
+ /** Undefined means the explicit control origin is safe to receive the builder credential. */
31
+ export function controlOriginRefusal(value, configured = process.env.XEER_MCP_CONTROL_ORIGINS) {
32
+ if (value === undefined)
33
+ return undefined;
34
+ const origin = exactHttpOrigin(value);
35
+ if (!origin) {
36
+ return 'The MCP controlUrl must be an exact HTTP(S) origin with no credentials, path, query, or fragment.';
37
+ }
38
+ if (origin === DEFAULT_XEER_CONTROL_ORIGIN || configuredControlOrigins(configured).has(origin))
39
+ return undefined;
40
+ return 'The MCP controlUrl is not allowed to receive builder credentials. Use https://control.xeer.run, '
41
+ + 'or preconfigure the exact HTTPS or loopback development origin in XEER_MCP_CONTROL_ORIGINS.';
42
+ }
43
+ /** Undefined means the target cannot make MCP perform an arbitrary direct network read. */
44
+ export function inspectorTargetRefusal(value, activeLocalOrigins = [], configured = process.env.XEER_MCP_INSPECTOR_ORIGINS) {
45
+ const target = value.trim();
46
+ if (!target)
47
+ return 'The inspector target must not be empty.';
48
+ if (isLocalInspectorTarget(target)) {
49
+ const origin = new URL(target).origin;
50
+ const allowed = new Set([
51
+ ...activeLocalOrigins.map((entry) => exactHttpOrigin(entry)).filter((entry) => entry !== undefined),
52
+ ...configuredInspectorOrigins(configured),
53
+ ]);
54
+ if (allowed.has(origin))
55
+ return undefined;
56
+ return 'The loopback inspector origin is not an active MCP dev session. Start it with xeer_dev_start '
57
+ + 'or preconfigure the exact origin in XEER_MCP_INSPECTOR_ORIGINS.';
58
+ }
59
+ // Hosted URLs are not fetched directly: the CLI encodes them into the authenticated control-plane
60
+ // lookup path, which owner-resolves the application before proxying its inspector.
61
+ if (isHostedXeerApplicationTarget(target) || APP_ID.test(target) || APPLICATION_NAME.test(target))
62
+ return undefined;
63
+ return 'MCP inspector URLs must be loopback HTTP URLs from xeer dev/preview or exact hosted '
64
+ + 'https://<app>.xeer.run origins. Use the application name or appId for a custom hosted domain.';
65
+ }
package/dist/server.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type XeerMcpProfile } from '@impetik/xeer-spec/actions';
2
3
  import { DevSessionRegistry } from './dev-session.js';
3
4
  export interface XeerMcpServer {
4
5
  server: McpServer;
5
6
  devSessions: DevSessionRegistry;
7
+ profile: XeerMcpProfile;
6
8
  }
7
- export declare function createXeerMcpServer(): XeerMcpServer;
9
+ export interface XeerMcpServerOptions {
10
+ readonly profile?: XeerMcpProfile;
11
+ }
12
+ export declare function mcpProfile(value: string | undefined): XeerMcpProfile;
13
+ export declare function createXeerMcpServer(options?: XeerMcpServerOptions): XeerMcpServer;
package/dist/server.js CHANGED
@@ -1,24 +1,86 @@
1
1
  import { createRequire } from 'node:module';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { isAbsolute, relative, sep } from 'node:path';
3
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { z } from 'zod';
4
- import { DEFAULT_TEMPLATE, TEMPLATES } from '@impetik/xeer';
5
+ import { createAgentContext, readDocsIndex, readDocsPage, searchDocs } from '@impetik/xeer/agent-context';
5
6
  import { DIAGNOSTICS, DIAGNOSTIC_CODES, DIAGNOSTIC_FAMILIES, diagnosticDefinition, renderDiagnosticsReference, } from '@impetik/xeer-spec/diagnostics';
7
+ import { actionDefinition, mcpAnnotations, } from '@impetik/xeer-spec/actions';
8
+ import { isReviewReceiptId } from '@impetik/xeer-spec/review';
6
9
  import { DevSessionRegistry } from './dev-session.js';
10
+ import { controlOriginRefusal, inspectorTargetRefusal } from './network-policy.js';
7
11
  import { runXeerTests } from './test-run.js';
8
- import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError } from './xeer-cli.js';
12
+ import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError, } from './xeer-cli.js';
9
13
  /**
10
14
  * The Xeer MCP server.
11
15
  *
12
16
  * Tools are a thin, honest projection of the CLI: one tool per command, each
13
- * returning the `xeer.command.v0` envelope the CLI printed. The only tools that
17
+ * preserving the `xeer.command.v0` envelope inside `xeer.mcp-result.v0`. The tools that
14
18
  * are not a single command are the dev-session trio, because a long-running
15
19
  * event stream has to be addressed by a handle and a cursor, and
16
20
  * `xeer_diagnostics`, which serves the generated catalogue so an agent can look
17
21
  * up a code without a second round trip through the filesystem.
18
22
  */
19
23
  const version = createRequire(import.meta.url)('../package.json').version;
20
- const directoryArgument = z.string().optional()
21
- .describe('Project directory. Relative paths resolve against the server root; defaults to it.');
24
+ function inputPropertySchema(property) {
25
+ let schema;
26
+ if (property.type === 'string') {
27
+ if (property.enum?.length === 0)
28
+ throw new Error('MCP input enum must contain at least one value.');
29
+ if (property.enum) {
30
+ if (property.minLength !== undefined || property.maxLength !== undefined || property.pattern !== undefined) {
31
+ throw new Error('MCP string enum inputs cannot also declare length or pattern constraints.');
32
+ }
33
+ schema = z.enum(property.enum);
34
+ }
35
+ else {
36
+ let string = z.string();
37
+ if (property.minLength !== undefined)
38
+ string = string.min(property.minLength);
39
+ if (property.maxLength !== undefined)
40
+ string = string.max(property.maxLength);
41
+ if (property.pattern !== undefined)
42
+ string = string.regex(new RegExp(property.pattern, 'u'));
43
+ schema = string;
44
+ }
45
+ }
46
+ else if (property.type === 'boolean') {
47
+ schema = z.boolean();
48
+ }
49
+ else {
50
+ let number = z.number();
51
+ if (property.type === 'integer')
52
+ number = number.int();
53
+ if (property.minimum !== undefined)
54
+ number = number.min(property.minimum);
55
+ if (property.maximum !== undefined)
56
+ number = number.max(property.maximum);
57
+ schema = number;
58
+ }
59
+ if (property.description !== undefined)
60
+ schema = schema.describe(property.description);
61
+ return schema;
62
+ }
63
+ /** Runtime MCP presentation and validation come from the registry projected by listTools and the docs. */
64
+ function toolPolicy(id) {
65
+ const definition = actionDefinition(id);
66
+ const registered = definition?.surfaces.mcpInputSchema;
67
+ if (!registered)
68
+ throw new Error(`Action ${id} has no registered MCP input schema.`);
69
+ const title = definition.surfaces.mcpTitle;
70
+ const description = definition.surfaces.mcpDescription;
71
+ if (!title || !description)
72
+ throw new Error(`Action ${id} has no registered MCP presentation.`);
73
+ const required = new Set(registered.required ?? []);
74
+ const inputSchema = Object.fromEntries(Object.entries(registered.properties).map(([name, property]) => {
75
+ let schema = inputPropertySchema(property);
76
+ if (property.default !== undefined)
77
+ schema = schema.default(property.default);
78
+ else if (!required.has(name))
79
+ schema = schema.optional();
80
+ return [name, schema];
81
+ }));
82
+ return { title, description, inputSchema, annotations: mcpAnnotations(definition) };
83
+ }
22
84
  /** Mirrors the Diagnostic interface in @impetik/xeer-spec, loosely so new fields pass through. */
23
85
  const diagnosticSchema = z.object({
24
86
  code: z.string(),
@@ -32,107 +94,232 @@ const diagnosticSchema = z.object({
32
94
  }).loose().optional(),
33
95
  hint: z.string().optional(),
34
96
  }).loose();
35
- const envelopeOutput = {
36
- argv: z.array(z.string()).describe('The exact xeer argv that produced this envelope.'),
37
- exitCode: z.number().nullable(),
97
+ const commandEnvelopeOutput = {
38
98
  protocol: z.literal('xeer.command.v0'),
39
99
  command: z.string(),
40
100
  ok: z.boolean(),
41
101
  diagnostics: z.array(diagnosticSchema),
42
102
  result: z.unknown().optional(),
43
- stderr: z.string().optional(),
44
103
  };
45
- function structured(payload) {
104
+ const mcpErrorSchema = z.object({ code: z.string(), message: z.string() });
105
+ const wrappedOutput = (result) => ({
106
+ protocol: z.literal('xeer.mcp-result.v0'),
107
+ action: z.string(),
108
+ ok: z.boolean(),
109
+ result: result.optional(),
110
+ error: mcpErrorSchema.optional(),
111
+ });
112
+ const commandOutput = wrappedOutput(z.object({
113
+ exitCode: z.number().nullable(),
114
+ envelope: z.object(commandEnvelopeOutput).loose(),
115
+ }));
116
+ function structured(action, result, options = {}) {
117
+ const reusableResult = reusableValue(result);
118
+ const payload = {
119
+ protocol: 'xeer.mcp-result.v0',
120
+ action,
121
+ ok: options.ok ?? true,
122
+ ...(reusableResult === undefined ? {} : { result: reusableResult }),
123
+ ...(options.error ? { error: reusableValue(options.error) } : {}),
124
+ };
46
125
  return {
47
126
  // The text block is the same JSON, because a client that ignores
48
- // structuredContent must still see the whole envelope.
127
+ // structuredContent must still see the whole wrapper.
49
128
  content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
50
129
  structuredContent: payload,
51
- ...(payload['ok'] === false ? { isError: false } : {}),
130
+ ...(payload.ok === false ? { isError: false } : {}),
52
131
  };
53
132
  }
54
- function failure(message) {
55
- return { content: [{ type: 'text', text: message }], isError: true };
133
+ function failure(action, code, message) {
134
+ return structured(action, undefined, { ok: false, error: { code, message } });
135
+ }
136
+ function escapeRegularExpression(value) {
137
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
138
+ }
139
+ /** Removes the configured host root from every public string, including nested diagnostics/events. */
140
+ function reusableString(value) {
141
+ const root = projectRoot();
142
+ const roots = new Set([root, root.split('\\').join('/'), root.split('/').join('\\')]);
143
+ let output = value;
144
+ for (const candidate of roots) {
145
+ output = output.replace(new RegExp(escapeRegularExpression(candidate), 'gi'), '.');
146
+ }
147
+ return output;
148
+ }
149
+ function reusableValue(value) {
150
+ if (typeof value === 'string')
151
+ return reusableString(value);
152
+ if (Array.isArray(value))
153
+ return value.map(reusableValue);
154
+ if (typeof value !== 'object' || value === null)
155
+ return value;
156
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, reusableValue(entry)]));
157
+ }
158
+ function projectPath(path) {
159
+ const output = relative(projectRoot(), path).split(sep).join('/');
160
+ return output || '.';
161
+ }
162
+ function reusableEnvelope(envelope) {
163
+ if (envelope.command !== 'new' || typeof envelope.result !== 'object' || envelope.result === null)
164
+ return envelope;
165
+ const result = envelope.result;
166
+ return {
167
+ ...envelope,
168
+ result: {
169
+ ...result,
170
+ ...(typeof result['directory'] === 'string'
171
+ ? { directory: isAbsolute(result['directory']) ? projectPath(result['directory']) : result['directory'] }
172
+ : {}),
173
+ },
174
+ };
56
175
  }
57
- /** Runs a CLI command and shapes the result identically for every tool. */
58
- async function commandTool(args, directory) {
59
- const run = await runXeerCommand(args, { cwd: directory });
60
- return structured({
61
- argv: run.argv,
176
+ /** Runs a CLI command and wraps its envelope without exposing host argv or stderr. */
177
+ async function commandTool(action, args, directory) {
178
+ let run;
179
+ try {
180
+ run = await runXeerCommand(args, { cwd: directory });
181
+ }
182
+ catch (error) {
183
+ if (error instanceof XeerCliError)
184
+ return failure(action, 'adapter_unavailable', error.message);
185
+ return failure(action, 'adapter_failure', 'The xeer CLI adapter failed before producing a result.');
186
+ }
187
+ const envelope = reusableEnvelope(run.envelope);
188
+ const adapterDiagnostic = envelope.diagnostics.find((diagnostic) => diagnostic.code === 'XE0000');
189
+ return structured(action, {
62
190
  exitCode: run.exitCode,
63
- ...run.envelope,
64
- ...(run.stderr ? { stderr: run.stderr } : {}),
191
+ envelope,
192
+ }, {
193
+ ok: envelope.ok,
194
+ ...(adapterDiagnostic ? {
195
+ error: { code: 'adapter_failure', message: adapterDiagnostic.message },
196
+ } : {}),
65
197
  });
66
198
  }
67
- async function withDirectory(directory, body) {
199
+ async function withDirectory(directory, action, body) {
200
+ let resolved;
68
201
  try {
69
- return await body(await resolveDirectory(directory));
202
+ resolved = await resolveDirectory(directory);
70
203
  }
71
204
  catch (error) {
72
205
  if (error instanceof XeerCliError)
73
- return failure(error.message);
206
+ return failure(action, 'path_refused', error.message);
74
207
  throw error;
75
208
  }
209
+ return body(resolved);
210
+ }
211
+ export function mcpProfile(value) {
212
+ if (value === undefined || value.trim() === '')
213
+ return 'author';
214
+ if (value === 'author' || value === 'operator')
215
+ return value;
216
+ throw new Error(`Invalid XEER_MCP_PROFILE ${JSON.stringify(value)}; expected "author" or "operator".`);
76
217
  }
77
- const CHECK_DESCRIPTION = [
78
- 'Validate a Xeer project and return its diagnostics as JSON. This is the repair loop:',
79
- 'read diagnostics, apply the smallest edit each one names, run it again, converge on ok: true.',
80
- 'Runs the manifest stage and, if that passes, the module-graph/zone/operations/type analysis.',
81
- 'Envelope: { protocol: "xeer.command.v0", command: "check", ok, diagnostics: Diagnostic[],',
82
- 'result?: { manifest } }. Diagnostic codes are stable — look one up with xeer_diagnostics.',
83
- ].join(' ');
84
- const BUILD_DESCRIPTION = [
85
- 'Build the content-addressed artifact. Runs check first, so build diagnostics are a superset',
86
- 'of check diagnostics plus bundling, budget, and asset codes (XE14xx, XE15xx).',
87
- 'On success result is { artifactId, outputDirectory, modules, assets, operations }.',
88
- 'Run xeer_test before this to prove the application behaves, not just that it compiles.',
89
- ].join(' ');
90
- export function createXeerMcpServer() {
218
+ export function createXeerMcpServer(options = {}) {
219
+ const profile = options.profile ?? 'author';
91
220
  const server = new McpServer({ name: 'xeer', version }, {
92
221
  instructions: [
93
222
  'Xeer is a constrained full-stack platform whose diagnostics are its agent surface.',
94
- `Project root for this server: ${projectRoot()}.`,
223
+ 'This server is confined to its configured project root.',
95
224
  'Loop: xeer_new to scaffold, xeer_check after every edit, xeer_test to prove behaviour,',
96
- 'xeer_build before deploy. A green check means it compiles; only a green test means it works.',
97
- 'Command tools return the CLI\'s xeer.command.v0 envelope verbatim; dispatch on diagnostics[].code',
225
+ 'xeer_build, then xeer_deploy_preview. A green check means it compiles; only a green test means it works.',
226
+ 'The author profile cannot change production. Promotion is available only when a human starts',
227
+ 'the server with XEER_MCP_PROFILE=operator, and then requires an exact artifact id.',
228
+ 'Tools return xeer.mcp-result.v0; command results preserve the CLI xeer.command.v0 envelope',
229
+ 'under result.envelope. Dispatch on that envelope\'s diagnostics[].code',
98
230
  'and use xeer_diagnostics to learn what a code means and which edit closes it.',
99
231
  'Use xeer_dev_start/status/stop only when you need a running preview and inspector;',
100
232
  'a check-edit-check loop plus xeer_test is enough to build and repair a project.',
101
233
  ].join(' '),
102
234
  });
103
235
  const devSessions = new DevSessionRegistry();
236
+ const jsonResource = (uri, value) => ({
237
+ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(reusableValue(value), null, 2) }],
238
+ });
239
+ const contextResource = async (uri, select) => jsonResource(uri, select(await createAgentContext(projectRoot())));
240
+ server.registerResource('project-context', 'xeer://project/context', {
241
+ title: 'Normalized Xeer project context', description: 'Bounded, deterministic, read-only AgentContextV0.',
242
+ mimeType: 'application/json',
243
+ }, async (uri) => contextResource(uri, (context) => context));
244
+ server.registerResource('project-manifest', 'xeer://project/manifest', {
245
+ title: 'Normalized application manifest', mimeType: 'application/json',
246
+ }, async (uri) => contextResource(uri, (context) => ({
247
+ ...context.application,
248
+ capabilities: context.capabilities,
249
+ storage: context.storage,
250
+ budgets: context.budgets,
251
+ database: context.schema,
252
+ })));
253
+ server.registerResource('project-operations', 'xeer://project/operations', {
254
+ title: 'Discovered application operations', mimeType: 'application/json',
255
+ }, async (uri) => contextResource(uri, (context) => context.operations));
256
+ server.registerResource('project-tests', 'xeer://project/tests', {
257
+ title: 'Application test inventory', mimeType: 'application/json',
258
+ }, async (uri) => contextResource(uri, (context) => context.tests));
259
+ server.registerResource('docs-index', 'xeer://docs/index', {
260
+ title: 'Installed Xeer documentation index', description: 'Compact version-matched page and content-hash index.',
261
+ mimeType: 'application/json',
262
+ }, async (uri) => {
263
+ const docs = await readDocsIndex();
264
+ return jsonResource(uri, {
265
+ ...docs.index,
266
+ ...(docs.compatibilityWarning ? { compatibilityWarning: docs.compatibilityWarning } : {}),
267
+ });
268
+ });
269
+ const docsTemplate = new ResourceTemplate('xeer://docs/{+path}', {
270
+ list: async () => {
271
+ const { index } = await readDocsIndex();
272
+ return { resources: index.pages.map((page) => ({
273
+ uri: `xeer://docs${page.path === '/' ? '/' : page.path}`,
274
+ name: page.title,
275
+ title: page.title,
276
+ description: page.description,
277
+ mimeType: 'text/markdown',
278
+ })) };
279
+ },
280
+ });
281
+ server.registerResource('docs-page', docsTemplate, {
282
+ title: 'Installed Xeer documentation page', description: 'A version-matched Markdown page loaded on demand.',
283
+ mimeType: 'text/markdown',
284
+ }, async (uri, variables) => {
285
+ const raw = variables.path;
286
+ const path = Array.isArray(raw) ? raw.join('/') : String(raw ?? '');
287
+ const page = await readDocsPage(path);
288
+ return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: page.markdown }] };
289
+ });
290
+ const diagnosticsTemplate = new ResourceTemplate('xeer://diagnostics/{code}', { list: undefined });
291
+ server.registerResource('diagnostic', diagnosticsTemplate, {
292
+ title: 'Xeer diagnostic definition', mimeType: 'application/json',
293
+ }, async (uri, variables) => {
294
+ const code = String(variables.code ?? '').toUpperCase();
295
+ const definition = diagnosticDefinition(code);
296
+ if (!definition)
297
+ throw new Error(`Unknown Xeer diagnostic code: ${code}`);
298
+ return jsonResource(uri, definition);
299
+ });
300
+ server.registerTool('xeer_agent_context', {
301
+ ...toolPolicy('agent.context'),
302
+ outputSchema: wrappedOutput(z.object({ protocol: z.literal('xeer.agent-context.v0') }).loose()),
303
+ }, async ({ directory }) => withDirectory(directory, 'agent.context', async (resolved) => structured('agent.context', await createAgentContext(resolved))));
304
+ server.registerTool('xeer_docs_search', {
305
+ ...toolPolicy('docs.search'),
306
+ outputSchema: wrappedOutput(z.object({
307
+ protocol: z.literal('xeer.docs-search.v0'), query: z.string(), results: z.array(z.object({
308
+ path: z.string(), uri: z.string(), title: z.string(), canonicalUrl: z.string(), contentHash: z.string(), score: z.number(),
309
+ }).loose()),
310
+ }).loose()),
311
+ }, async ({ query, limit }) => structured('docs.search', await searchDocs(query, limit)));
104
312
  server.registerTool('xeer_check', {
105
- title: 'Check a Xeer project',
106
- description: CHECK_DESCRIPTION,
107
- inputSchema: { directory: directoryArgument },
108
- outputSchema: envelopeOutput,
109
- annotations: { readOnlyHint: true, openWorldHint: false },
110
- }, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['check', resolved], resolved)));
313
+ ...toolPolicy('check'),
314
+ outputSchema: commandOutput,
315
+ }, async ({ directory }) => withDirectory(directory, 'check', (resolved) => commandTool('check', ['check', resolved], resolved)));
111
316
  server.registerTool('xeer_build', {
112
- title: 'Build a Xeer project',
113
- description: BUILD_DESCRIPTION,
114
- inputSchema: { directory: directoryArgument },
115
- outputSchema: envelopeOutput,
116
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
117
- }, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['build', resolved], resolved)));
317
+ ...toolPolicy('build'),
318
+ outputSchema: commandOutput,
319
+ }, async ({ directory }) => withDirectory(directory, 'build', (resolved) => commandTool('build', ['build', resolved], resolved)));
118
320
  server.registerTool('xeer_test', {
119
- title: 'Run a Xeer project\'s tests',
120
- description: [
121
- 'Run the application\'s own tests: build, type-check tests/**/*.test.ts against the generated',
122
- 'contract, boot the verified artifact in workerd with fresh isolated state, run every test, tear',
123
- 'down. This is the convergence check — a green check proves the project compiles, a green test',
124
- 'proves it behaves. Returns { ok, total, passed, failed, cases, diagnostics, events }.',
125
- 'Each failed case carries a Diagnostic-shaped failure with matcher, expected, actual, and the',
126
- 'project-relative test location: XE1904 assertion, XE1905 threw or unexpectedly refused call,',
127
- 'XE1906 timeout. Compiler diagnostics abort the run before any test boots.',
128
- 'Tests call as(\'alice\'|\'bob\'|\'guest\'), so ownership and authorization are directly testable.',
129
- ].join(' '),
130
- inputSchema: {
131
- directory: directoryArgument,
132
- timeoutMilliseconds: z.number().int().min(1_000).max(1_800_000).optional(),
133
- },
134
- outputSchema: {
135
- argv: z.array(z.string()),
321
+ ...toolPolicy('test'),
322
+ outputSchema: wrappedOutput(z.object({
136
323
  exitCode: z.number().nullable(),
137
324
  ok: z.boolean(),
138
325
  reason: z.string().optional(),
@@ -156,103 +343,80 @@ export function createXeerMcpServer() {
156
343
  type: z.string(),
157
344
  data: z.unknown(),
158
345
  }).loose()),
159
- stderr: z.string().optional(),
160
- },
161
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
162
- }, async ({ directory, timeoutMilliseconds }) => withDirectory(directory, async (resolved) => {
163
- const run = await runXeerTests({
164
- directory: resolved,
165
- ...(timeoutMilliseconds === undefined ? {} : { timeoutMilliseconds }),
346
+ }).loose()),
347
+ }, async ({ directory, timeoutMilliseconds }) => withDirectory(directory, 'test', async (resolved) => {
348
+ let run;
349
+ try {
350
+ run = await runXeerTests({
351
+ directory: resolved,
352
+ ...(timeoutMilliseconds === undefined ? {} : { timeoutMilliseconds }),
353
+ });
354
+ }
355
+ catch (error) {
356
+ if (error instanceof XeerCliError)
357
+ return failure('test', 'adapter_unavailable', error.message);
358
+ return failure('test', 'adapter_failure', 'The xeer test adapter failed before producing a result.');
359
+ }
360
+ const { argv: _argv, stderr: _stderr, ...reusable } = run;
361
+ const adapterDiagnostic = run.diagnostics.find((diagnostic) => diagnostic.code === 'XE0000');
362
+ return structured('test', reusable, {
363
+ ok: run.ok,
364
+ ...(adapterDiagnostic ? {
365
+ error: { code: 'adapter_failure', message: adapterDiagnostic.message },
366
+ } : {}),
166
367
  });
167
- return structured(run);
168
368
  }));
169
369
  server.registerTool('xeer_new', {
170
- title: 'Scaffold a Xeer project',
171
- description: 'Create a new Xeer project in an empty or non-existent directory. '
172
- + 'Every template checks, tests, and builds clean, so use one as the starting point rather '
173
- + 'than writing a manifest by hand. "notes" (the default) and "todo" are per-user apps, "blog" '
174
- + 'is public to read and private to write, and "personal-site" has no database at all. None of '
175
- + 'them scaffolds sign-in UI. result is { directory, name, template, files }; an unknown '
176
- + 'template is XE3002 and writes nothing.',
177
- inputSchema: {
178
- directory: z.string().describe('Target directory, relative to the server root. Must be empty or absent.'),
179
- // The names come from the CLI itself, so this schema cannot advertise a template the
180
- // installed CLI does not have — or omit one it does.
181
- template: z.enum(TEMPLATES).optional()
182
- .describe(`Which scaffold to write. Defaults to ${DEFAULT_TEMPLATE}.`),
183
- },
184
- outputSchema: envelopeOutput,
185
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
186
- }, async ({ directory, template }) => withDirectory(directory, (resolved) => commandTool(['new', resolved, ...(template === undefined ? [] : ['--template', template])], projectRoot())));
370
+ ...toolPolicy('new'),
371
+ outputSchema: commandOutput,
372
+ }, async ({ directory, template }) => withDirectory(directory, 'new', (resolved) => commandTool('new', ['new', resolved, ...(template === undefined ? [] : ['--template', template])], projectRoot())));
187
373
  server.registerTool('xeer_doctor', {
188
- title: 'Diagnose the Xeer toolchain',
189
- description: 'Check the local toolchain and generated-file state. Use it when a command fails for '
190
- + 'a reason no project edit explains. result is { checks, summary }; failures also appear as XE4001 '
191
- + 'diagnostics.',
192
- inputSchema: { directory: directoryArgument },
193
- outputSchema: envelopeOutput,
194
- annotations: { readOnlyHint: true, openWorldHint: false },
195
- }, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['doctor', resolved], resolved)));
196
- server.registerTool('xeer_deploy', {
197
- title: 'Deploy a Xeer project',
198
- description: 'Build, verify, and upload the artifact to the control plane. Requires a builder '
199
- + 'credential: XE5002 means a human must run `xeer auth login` first. result is '
200
- + '{ application, url, ... }. Defaults to production; pass environment "preview" to deploy to the '
201
- + 'app\'s side-by-side preview URL instead, which is what to iterate against — production keeps '
202
- + 'serving, and preview has its own disposable state. Make a preview live with xeer_promote.',
203
- inputSchema: {
204
- directory: directoryArgument,
205
- environment: z.enum(['production', 'preview']).optional()
206
- .describe('Deployment target. Omit for production; "preview" leaves production untouched.'),
207
- controlUrl: z.string().optional().describe('Control-plane origin override, e.g. https://control.example.com.'),
208
- },
209
- outputSchema: envelopeOutput,
210
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
211
- }, async ({ directory, environment, controlUrl }) => withDirectory(directory, (resolved) => commandTool(['deploy', resolved, ...(environment ? ['--environment', environment] : []),
212
- ...(controlUrl ? ['--control-url', controlUrl] : [])], resolved)));
213
- server.registerTool('xeer_promote', {
214
- title: 'Promote a Xeer preview to production',
215
- description: 'Make the version currently on the app\'s preview URL live on production — the same '
216
- + 'bundle, not a rebuild, with production\'s own environment values. The second half of the '
217
- + '"deploy to preview, look at it, then ship it" loop. XE5175 means nothing has been deployed to '
218
- + 'preview yet. result is { artifactId, replaced, url, ... }.',
219
- inputSchema: {
220
- directory: directoryArgument,
221
- fromArtifact: z.string().optional()
222
- .describe('Promote this artifact id instead of whatever preview is serving, e.g. sha256:….'),
223
- controlUrl: z.string().optional().describe('Control-plane origin override.'),
224
- },
225
- outputSchema: envelopeOutput,
226
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
227
- }, async ({ directory, fromArtifact, controlUrl }) => withDirectory(directory, (resolved) => commandTool(['promote', resolved, ...(fromArtifact ? ['--from-artifact', fromArtifact] : []),
228
- ...(controlUrl ? ['--control-url', controlUrl] : [])], resolved)));
374
+ ...toolPolicy('doctor'),
375
+ outputSchema: commandOutput,
376
+ }, async ({ directory }) => withDirectory(directory, 'doctor', (resolved) => commandTool('doctor', ['doctor', resolved], resolved)));
377
+ server.registerTool('xeer_deploy_preview', {
378
+ ...toolPolicy('deploy.preview'),
379
+ outputSchema: commandOutput,
380
+ }, async ({ directory, controlUrl }) => {
381
+ const refusal = controlOriginRefusal(controlUrl);
382
+ if (refusal)
383
+ return failure('deploy.preview', 'url_refused', refusal);
384
+ return withDirectory(directory, 'deploy.preview', (resolved) => commandTool('deploy.preview', ['deploy', resolved, '--environment', 'preview', ...(controlUrl ? ['--control-url', controlUrl] : [])], resolved));
385
+ });
386
+ if (profile === 'operator') {
387
+ server.registerTool('xeer_promote', {
388
+ ...toolPolicy('promote.operator'),
389
+ outputSchema: commandOutput,
390
+ }, async ({ directory, receiptId, controlUrl }) => {
391
+ if (!isReviewReceiptId(receiptId)) {
392
+ return failure('promote.operator', 'invalid_review_receipt_id', 'receiptId must be the complete review_… id returned by a preview deployment.');
393
+ }
394
+ const refusal = controlOriginRefusal(controlUrl);
395
+ if (refusal)
396
+ return failure('promote.operator', 'url_refused', refusal);
397
+ return withDirectory(directory, 'promote.operator', (resolved) => commandTool('promote.operator', ['promote', resolved, '--receipt', receiptId,
398
+ ...(controlUrl ? ['--control-url', controlUrl] : [])], resolved));
399
+ });
400
+ }
229
401
  server.registerTool('xeer_auth_status', {
230
- title: 'Report builder sign-in status',
231
- description: 'Report whether a builder credential is available for deployment. Sign-in itself is '
232
- + 'interactive and cannot be completed by an agent; XE5002 means ask the human to run '
233
- + '`xeer auth login`.',
234
- inputSchema: {
235
- controlUrl: z.string().optional().describe('Control-plane origin override.'),
236
- },
237
- outputSchema: envelopeOutput,
238
- annotations: { readOnlyHint: true, openWorldHint: true },
239
- }, async ({ controlUrl }) => commandTool(['auth', 'status', ...(controlUrl ? ['--control-url', controlUrl] : [])], projectRoot()));
402
+ ...toolPolicy('auth.status'),
403
+ outputSchema: commandOutput,
404
+ }, async ({ controlUrl }) => {
405
+ const refusal = controlOriginRefusal(controlUrl);
406
+ if (refusal)
407
+ return failure('auth.status', 'url_refused', refusal);
408
+ return commandTool('auth.status', ['auth', 'status', ...(controlUrl ? ['--control-url', controlUrl] : [])], projectRoot());
409
+ });
240
410
  server.registerTool('xeer_inspect', {
241
- title: 'Inspect a running preview',
242
- description: 'Read the inspector API of a running dev or preview server: normalized manifest '
243
- + '(manifest), per-table record counts (state), the bounded structured log ring (logs), or '
244
- + 'every stored record with the schema that describes it (export). Use the URLs from '
245
- + 'xeer_dev_status.preview.',
246
- inputSchema: {
247
- previewUrl: z.string().describe('Preview URL from preview.ready, e.g. http://127.0.0.1:43127/.'),
248
- view: z.enum(['manifest', 'state', 'logs', 'export']).default('manifest'),
249
- after: z.string().optional().describe('Log cursor, for view: "logs".'),
250
- },
251
- outputSchema: envelopeOutput,
252
- annotations: { readOnlyHint: true, openWorldHint: true },
411
+ ...toolPolicy('inspect'),
412
+ outputSchema: commandOutput,
253
413
  }, async ({ previewUrl, view, after }) => {
414
+ const activeLocalOrigins = devSessions.summaries().flatMap((session) => session.running && session.preview ? [session.preview.url] : []);
415
+ const refusal = inspectorTargetRefusal(previewUrl, activeLocalOrigins);
416
+ if (refusal)
417
+ return failure('inspect', 'url_refused', refusal);
254
418
  const command = view === 'manifest' ? 'inspect' : view;
255
- return commandTool([command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
419
+ return commandTool('inspect', [command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
256
420
  });
257
421
  const sessionOutput = {
258
422
  sessionId: z.string(),
@@ -272,7 +436,6 @@ export function createXeerMcpServer() {
272
436
  openDiagnostics: z.array(diagnosticSchema),
273
437
  exit: z.object({ code: z.number().optional(), reason: z.string().optional() }).loose().optional(),
274
438
  droppedEvents: z.number(),
275
- stderr: z.string().optional(),
276
439
  events: z.array(z.object({
277
440
  protocol: z.literal('xeer.dev.v0'),
278
441
  seq: z.number(),
@@ -281,24 +444,17 @@ export function createXeerMcpServer() {
281
444
  data: z.unknown(),
282
445
  }).loose()),
283
446
  };
447
+ const reusableSession = (value) => {
448
+ const { stderr: _stderr, directory, ...rest } = value;
449
+ return {
450
+ ...rest,
451
+ ...(typeof directory === 'string' ? { directory: projectPath(directory) } : {}),
452
+ };
453
+ };
284
454
  server.registerTool('xeer_dev_start', {
285
- title: 'Start a dev session',
286
- description: [
287
- 'Start `xeer dev --json` and return the xeer.dev.v0 events it emitted up to the point it settled.',
288
- 'Returns when preview.ready arrives (status "ready"), or when the first compile fails',
289
- '(status "compile_failed", openDiagnostics populated). Port 0 by default, so the real URL is in',
290
- 'preview.ready. Keep the returned cursor and pass it to xeer_dev_status after each edit.',
291
- 'You do not need a dev session to repair diagnostics: xeer_check is the same compiler.',
292
- ].join(' '),
293
- inputSchema: {
294
- directory: directoryArgument,
295
- host: z.string().optional().describe('Defaults to 127.0.0.1.'),
296
- port: z.number().int().min(0).max(65535).optional().describe('Defaults to 0 (OS-assigned).'),
297
- timeoutMilliseconds: z.number().int().min(1_000).max(600_000).optional(),
298
- },
299
- outputSchema: sessionOutput,
300
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
301
- }, async ({ directory, host, port, timeoutMilliseconds }) => withDirectory(directory, async (resolved) => {
455
+ ...toolPolicy('dev.start'),
456
+ outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
457
+ }, async ({ directory, host, port, timeoutMilliseconds }) => withDirectory(directory, 'dev.start', async (resolved) => {
302
458
  let session;
303
459
  try {
304
460
  session = devSessions.start({
@@ -308,71 +464,78 @@ export function createXeerMcpServer() {
308
464
  });
309
465
  }
310
466
  catch (error) {
311
- return failure(error instanceof Error ? error.message : String(error));
467
+ if (error instanceof XeerCliError)
468
+ return failure('dev.start', 'adapter_unavailable', error.message);
469
+ return failure('dev.start', 'session_refused', error instanceof Error ? error.message : String(error));
470
+ }
471
+ try {
472
+ await session.waitForStart(timeoutMilliseconds ?? 180_000);
473
+ }
474
+ catch {
475
+ return failure('dev.start', 'session_start_failed', 'The dev session failed while starting.');
476
+ }
477
+ if (session.status === 'starting') {
478
+ try {
479
+ await session.stop();
480
+ }
481
+ catch {
482
+ return failure('dev.start', 'session_stop_failed', 'The timed-out dev session could not be stopped.');
483
+ }
484
+ return structured('dev.start', reusableSession(session.read(0)), {
485
+ ok: false,
486
+ error: {
487
+ code: 'session_start_timeout',
488
+ message: `xeer dev did not become ready within ${timeoutMilliseconds ?? 180_000} ms.`,
489
+ },
490
+ });
312
491
  }
313
- await session.waitForStart(timeoutMilliseconds ?? 180_000);
314
- return structured(session.read(0));
492
+ const result = reusableSession(session.read(0));
493
+ return structured('dev.start', result, session.status === 'ready' ? {} : {
494
+ ok: false,
495
+ error: { code: 'session_start_failed', message: `xeer dev stopped with status ${session.status}.` },
496
+ });
315
497
  }));
316
498
  server.registerTool('xeer_dev_status', {
317
- title: 'Read new dev-session events',
318
- description: [
319
- 'Read the xeer.dev.v0 events emitted since `cursor`, then return the session summary.',
320
- 'After editing a file, call this with waitMilliseconds set: the CLI coalesces changes behind a',
321
- '75 ms quiet window, rebuilds, and emits compile.start, any compile.diagnostic, and compile.ready',
322
- 'when the rebuild is accepted. openDiagnostics is the outcome of the latest attempt, so an empty',
323
- 'openDiagnostics with a higher generation means the edit was accepted.',
324
- ].join(' '),
325
- inputSchema: {
326
- sessionId: z.string().optional().describe('Optional when only one session exists.'),
327
- cursor: z.number().int().min(0).default(0).describe('Last seq you have already read.'),
328
- waitMilliseconds: z.number().int().min(0).max(600_000).default(0)
329
- .describe('Wait up to this long for new events to arrive and settle.'),
330
- limit: z.number().int().min(1).max(1_000).default(200),
331
- },
332
- outputSchema: sessionOutput,
333
- annotations: { readOnlyHint: true, openWorldHint: false },
499
+ ...toolPolicy('dev.status'),
500
+ outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
334
501
  }, async ({ sessionId, cursor, waitMilliseconds, limit }) => {
335
502
  let session;
336
503
  try {
337
504
  session = devSessions.get(sessionId);
338
505
  }
339
506
  catch (error) {
340
- return failure(error instanceof Error ? error.message : String(error));
507
+ return failure('dev.status', 'session_refused', error instanceof Error ? error.message : String(error));
341
508
  }
342
- await session.waitForActivity(cursor, waitMilliseconds);
343
- return structured(session.read(cursor, limit));
509
+ try {
510
+ await session.waitForActivity(cursor, waitMilliseconds);
511
+ }
512
+ catch {
513
+ return failure('dev.status', 'session_read_failed', 'The dev session could not be read.');
514
+ }
515
+ return structured('dev.status', reusableSession(session.read(cursor, limit)));
344
516
  });
345
517
  server.registerTool('xeer_dev_stop', {
346
- title: 'Stop a dev session',
347
- description: 'Stop a dev session over the documented xeer.dev.control.v0 IPC shutdown, releasing '
348
- + 'the local state lease. Always stop sessions you started: the lease is per project and mode, '
349
- + 'so a leaked session blocks the next dev run with XE1812.',
350
- inputSchema: {
351
- sessionId: z.string().optional().describe('Optional when only one session exists.'),
352
- cursor: z.number().int().min(0).default(0),
353
- },
354
- outputSchema: sessionOutput,
355
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
518
+ ...toolPolicy('dev.stop'),
519
+ outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
356
520
  }, async ({ sessionId, cursor }) => {
357
521
  let session;
358
522
  try {
359
523
  session = devSessions.get(sessionId);
360
524
  }
361
525
  catch (error) {
362
- return failure(error instanceof Error ? error.message : String(error));
526
+ return failure('dev.stop', 'session_refused', error instanceof Error ? error.message : String(error));
527
+ }
528
+ try {
529
+ await session.stop();
363
530
  }
364
- await session.stop();
365
- return structured(session.read(cursor));
531
+ catch {
532
+ return failure('dev.stop', 'session_stop_failed', 'The dev session could not be stopped.');
533
+ }
534
+ return structured('dev.stop', reusableSession(session.read(cursor)));
366
535
  });
367
536
  server.registerTool('xeer_diagnostics', {
368
- title: 'Look up Xeer diagnostic codes',
369
- description: 'Explain a diagnostic code: what the platform observed and which edit closes it. '
370
- + 'Omit `code` for the whole generated reference. This is the same catalogue the compiler emits '
371
- + 'from, so it cannot drift from the codes you receive.',
372
- inputSchema: {
373
- code: z.string().optional().describe('An XE#### code, e.g. XE1202.'),
374
- },
375
- outputSchema: {
537
+ ...toolPolicy('diagnostics'),
538
+ outputSchema: wrappedOutput(z.object({
376
539
  code: z.string().optional(),
377
540
  family: z.string().optional(),
378
541
  means: z.string().optional(),
@@ -380,20 +543,19 @@ export function createXeerMcpServer() {
380
543
  surfaces: z.array(z.string()).optional(),
381
544
  codes: z.array(z.string()).optional(),
382
545
  reference: z.string().optional(),
383
- },
384
- annotations: { readOnlyHint: true, openWorldHint: false },
546
+ }).loose()),
385
547
  }, async ({ code }) => {
386
548
  if (!code) {
387
- return structured({ codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
549
+ return structured('diagnostics', { codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
388
550
  }
389
551
  const normalized = code.trim().toUpperCase();
390
552
  const definition = diagnosticDefinition(normalized);
391
553
  if (!definition) {
392
554
  const known = Object.keys(DIAGNOSTICS).sort().join(', ');
393
- return failure(`Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
555
+ return failure('diagnostics', 'unknown_diagnostic', `Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
394
556
  }
395
557
  const family = DIAGNOSTIC_FAMILIES.find((candidate) => candidate.prefix === definition.prefix);
396
- return structured({
558
+ return structured('diagnostics', {
397
559
  code: definition.code,
398
560
  family: family ? `${family.title}: ${family.summary}` : definition.prefix,
399
561
  means: definition.means,
@@ -401,5 +563,5 @@ export function createXeerMcpServer() {
401
563
  surfaces: [...definition.surfaces],
402
564
  });
403
565
  });
404
- return { server, devSessions };
566
+ return { server, devSessions, profile };
405
567
  }
package/dist/test-run.js CHANGED
@@ -93,7 +93,7 @@ export async function runXeerTests(options) {
93
93
  code: 'XE0000',
94
94
  severity: 'error',
95
95
  message: failure
96
- ?? (stderr.trim() || `xeer ${argv.join(' ')} exited with ${exitCode} and emitted no events.`),
96
+ ?? `xeer test --json exited with ${exitCode} and emitted no events.`,
97
97
  });
98
98
  }
99
99
  done({
@@ -112,7 +112,7 @@ export async function runXeerTests(options) {
112
112
  ...(stderr.trim() ? { stderr: stderr.trim() } : {}),
113
113
  });
114
114
  };
115
- child.on('error', (error) => settle(null, `Could not run the xeer CLI: ${error.message}`));
116
- child.on('close', (code) => settle(code, timedOut ? `xeer ${argv.join(' ')} exceeded ${timeout} ms and was killed.` : undefined));
115
+ child.on('error', () => settle(null, 'Could not run the xeer CLI.'));
116
+ child.on('close', (code) => settle(code, timedOut ? `xeer test --json exceeded ${timeout} ms and was killed.` : undefined));
117
117
  });
118
118
  }
@@ -1,7 +1,7 @@
1
1
  import type { Diagnostic } from '@impetik/xeer-spec';
2
2
  /**
3
3
  * Every tool in this server shells out to the `xeer` CLI with `--json` and
4
- * forwards the envelope it printed, unchanged.
4
+ * preserves the envelope it printed inside a versioned MCP result.
5
5
  *
6
6
  * That is deliberate. The CLI's `xeer.command.v0` envelope and its diagnostics
7
7
  * are the versioned contract (docs/specs/dev-protocol-v0.md); re-implementing
@@ -18,7 +18,7 @@ export interface CommandEnvelope {
18
18
  result?: unknown;
19
19
  }
20
20
  export interface CommandRun {
21
- /** Argv after the CLI path, so a human can reproduce the call. */
21
+ /** Internal argv after the CLI path. Never include it in an MCP result: it may contain host paths. */
22
22
  argv: string[];
23
23
  exitCode: number | null;
24
24
  envelope: CommandEnvelope;
package/dist/xeer-cli.js CHANGED
@@ -19,9 +19,9 @@ export function resolveXeerCli() {
19
19
  try {
20
20
  return resolve(dirname(require.resolve('@impetik/xeer')), 'cli.js');
21
21
  }
22
- catch (error) {
22
+ catch {
23
23
  throw new XeerCliError('Could not resolve the xeer CLI. Install @impetik/xeer alongside this server, '
24
- + `or set XEER_CLI to its dist/cli.js: ${error instanceof Error ? error.message : String(error)}`);
24
+ + 'or set XEER_CLI to its dist/cli.js.');
25
25
  }
26
26
  }
27
27
  /**
@@ -44,7 +44,7 @@ export async function resolveDirectory(directory) {
44
44
  const rootReal = await realpath(root).catch(() => root);
45
45
  const requestedReal = await realpath(requested).catch(() => requested);
46
46
  if (!contains(root, requested) || !contains(rootReal, requestedReal)) {
47
- throw new XeerCliError(`directory must stay inside ${root}. Received ${requested}. `
47
+ throw new XeerCliError('directory must stay inside the configured XEER_MCP_ROOT. '
48
48
  + 'Set XEER_MCP_ROOT when the server must reach another tree.');
49
49
  }
50
50
  return requested;
@@ -113,11 +113,11 @@ export async function runXeerCommand(args, options = {}) {
113
113
  argv,
114
114
  exitCode,
115
115
  envelope: envelope ?? crashEnvelope(command, failure
116
- ?? (stderr.trim() || `xeer ${argv.join(' ')} exited with ${exitCode} and printed no envelope.`)),
116
+ ?? `xeer ${command} --json exited with ${exitCode} and printed no envelope.`),
117
117
  ...(stderr.trim() ? { stderr: stderr.trim() } : {}),
118
118
  });
119
119
  };
120
- child.on('error', (error) => settle(null, `Could not run the xeer CLI: ${error.message}`));
121
- child.on('close', (code) => settle(code, timedOut ? `xeer ${argv.join(' ')} exceeded ${timeout} ms and was killed.` : undefined));
120
+ child.on('error', () => settle(null, 'Could not run the xeer CLI.'));
121
+ child.on('close', (code) => settle(code, timedOut ? `xeer ${command} --json exceeded ${timeout} ms and was killed.` : undefined));
122
122
  });
123
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impetik/xeer-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Model Context Protocol server for Xeer: the scaffold, check, dev, build, and deploy loop as agent tools.",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "zod": "^4.0.10",
46
- "@impetik/xeer": "0.2.3",
47
- "@impetik/xeer-spec": "0.2.3"
46
+ "@impetik/xeer": "0.2.5",
47
+ "@impetik/xeer-spec": "0.2.5"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^24.1.0"