@impetik/xeer-mcp 0.2.2 → 0.2.4

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,13 +1,22 @@
1
- # @impetik/xeer-mcp
1
+ # `@impetik/xeer-mcp`
2
2
 
3
- Model Context Protocol server for [Xeer](https://github.com/impetik/xeer). It exposes the
4
- scaffold → check → dev → build → deploy loop as MCP tools, each returning the
5
- `xeer.command.v0` JSON envelope the `xeer` CLI already prints.
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 with a
5
+ stable `xeer.mcp-result.v0` result protocol.
6
6
 
7
- See [docs/AGENTS.md](https://github.com/impetik/xeer/blob/main/docs/AGENTS.md) for the loop
8
- this server is built for, and the `xeer` skill in
9
- [`.claude/skills/xeer/`](https://github.com/impetik/xeer/tree/main/.claude/skills/xeer) for
10
- the same protocol written as agent instructions.
7
+ 📖 **Documentation: [docs.xeer.run](https://docs.xeer.run)** · **Building with AI agents:
8
+ [docs.xeer.run/guides/agents](https://docs.xeer.run/guides/agents)** · **Diagnostics:
9
+ [docs.xeer.run/reference/diagnostics](https://docs.xeer.run/reference/diagnostics)**
10
+
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.
11
20
 
12
21
  ## Run it
13
22
 
@@ -17,40 +26,99 @@ the same protocol written as agent instructions.
17
26
  "mcpServers": {
18
27
  "xeer": {
19
28
  "command": "npx",
20
- "args": ["--package=@impetik/xeer-mcp", "--", "xeer-mcp"],
29
+ "args": ["--package=@impetik/xeer-mcp@<XEER_VERSION>", "--", "xeer-mcp"],
21
30
  "env": { "XEER_MCP_ROOT": "." }
22
31
  }
23
32
  }
24
33
  }
25
34
  ```
26
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
+
27
46
  From this repository, after `pnpm -r build`:
28
47
 
29
48
  ```sh
30
49
  node packages/mcp/dist/main.js # stdio; or `pnpm mcp` from the workspace root
31
50
  ```
32
51
 
33
- ## Tools
34
-
35
- | Tool | CLI it runs | Returns |
36
- | --- | --- | --- |
37
- | `xeer_check` | `xeer check <dir> --json` | Manifest + analysis diagnostics; `result.manifest` when clean |
38
- | `xeer_test` | `xeer test <dir> --json` | `{ ok, total, passed, failed, cases, diagnostics, events }` |
39
- | `xeer_build` | `xeer build <dir> --json` | `result.artifactId`, modules, assets, operations |
40
- | `xeer_new` | `xeer new <dir> --json` | `result.files` |
41
- | `xeer_doctor` | `xeer doctor <dir> --json` | `result.checks`, `result.summary` |
42
- | `xeer_deploy` | `xeer deploy <dir> --json` | `result.url` |
43
- | `xeer_auth_status` | `xeer auth status --json` | Builder identity and credential expiry |
44
- | `xeer_inspect` | `xeer inspect`/`state`/`logs` | Inspector response for a running preview |
45
- | `xeer_dev_start` | `xeer dev <dir> --json --port 0` | Session handle, cursor, and the events up to `preview.ready` |
46
- | `xeer_dev_status` | — | `xeer.dev.v0` events after a cursor, plus `openDiagnostics` |
47
- | `xeer_dev_stop` | — | Final events; shuts down over `xeer.dev.control.v0` IPC |
48
- | `xeer_diagnostics` | — | What an `XE####` code means and the edit that closes it |
49
-
50
- Every command tool returns the envelope both as `structuredContent` and as a JSON text
51
- block, so a client that ignores structured output still sees all of it. A tool result is
52
- always an envelope: when the CLI crashes without printing one, the server synthesizes
53
- `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.
54
122
 
55
123
  `xeer_test` is the exception, because `xeer test --json` streams `xeer.dev.v0` events rather
56
124
  than printing an envelope. It collects the run and returns the summary plus the unabridged
@@ -59,11 +127,6 @@ assertion, `XE1905` throw or unexpectedly refused call, `XE1906` timeout) that i
59
127
  matcher, expected and actual values, and the project-relative test location. It needs no
60
128
  session handle: unlike `dev`, a test run is short-lived and terminal.
61
129
 
62
- `xeer link` and `xeer env` are deliberately absent. Which hosted app a checkout deploys to
63
- is the human owner's decision, so on `XE5111`/`XE5120`/`XE5121` the surface reports the
64
- diagnostic and its documented repair instead of performing it. `xeer env` reads and writes
65
- secrets — `xeer env pull` returns plaintext — so it is not reachable through a tool call.
66
-
67
130
  ## Why dev is three tools
68
131
 
69
132
  An MCP tool call is request/response; `xeer dev` is a long-lived JSONL stream. The session
@@ -71,10 +134,10 @@ is therefore owned by the server and addressed by a `sessionId`, with the protoc
71
134
  `seq` as a resumable cursor:
72
135
 
73
136
  ```text
74
- xeer_dev_start → { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events }
137
+ xeer_dev_start → { result: { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events } }
75
138
  edit a file
76
- xeer_dev_status → { events: [compile.start, compile.diagnostic?, compile.ready?], openDiagnostics, generation }
77
- 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' } }
78
141
  ```
79
142
 
80
143
  No event is summarized away, so the agent reads the same stream a terminal would show.
@@ -85,8 +148,18 @@ session exists for when the preview, the inspector, or rebuild/rollback behaviou
85
148
 
86
149
  | Variable | Effect |
87
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. |
88
152
  | `XEER_MCP_ROOT` | Confinement root for every `directory` argument. Defaults to the server's working directory; paths that escape it are refused. |
89
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.
90
163
 
91
164
  Always stop dev sessions you start: local state is leased per project and mode, so a leaked
92
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,23 +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';
5
+ import { createAgentContext, readDocsIndex, readDocsPage, searchDocs } from '@impetik/xeer/agent-context';
4
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';
5
9
  import { DevSessionRegistry } from './dev-session.js';
10
+ import { controlOriginRefusal, inspectorTargetRefusal } from './network-policy.js';
6
11
  import { runXeerTests } from './test-run.js';
7
- import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError } from './xeer-cli.js';
12
+ import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError, } from './xeer-cli.js';
8
13
  /**
9
14
  * The Xeer MCP server.
10
15
  *
11
16
  * Tools are a thin, honest projection of the CLI: one tool per command, each
12
- * 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
13
18
  * are not a single command are the dev-session trio, because a long-running
14
19
  * event stream has to be addressed by a handle and a cursor, and
15
20
  * `xeer_diagnostics`, which serves the generated catalogue so an agent can look
16
21
  * up a code without a second round trip through the filesystem.
17
22
  */
18
23
  const version = createRequire(import.meta.url)('../package.json').version;
19
- const directoryArgument = z.string().optional()
20
- .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
+ }
21
84
  /** Mirrors the Diagnostic interface in @impetik/xeer-spec, loosely so new fields pass through. */
22
85
  const diagnosticSchema = z.object({
23
86
  code: z.string(),
@@ -31,107 +94,232 @@ const diagnosticSchema = z.object({
31
94
  }).loose().optional(),
32
95
  hint: z.string().optional(),
33
96
  }).loose();
34
- const envelopeOutput = {
35
- argv: z.array(z.string()).describe('The exact xeer argv that produced this envelope.'),
36
- exitCode: z.number().nullable(),
97
+ const commandEnvelopeOutput = {
37
98
  protocol: z.literal('xeer.command.v0'),
38
99
  command: z.string(),
39
100
  ok: z.boolean(),
40
101
  diagnostics: z.array(diagnosticSchema),
41
102
  result: z.unknown().optional(),
42
- stderr: z.string().optional(),
43
103
  };
44
- 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
+ };
45
125
  return {
46
126
  // The text block is the same JSON, because a client that ignores
47
- // structuredContent must still see the whole envelope.
127
+ // structuredContent must still see the whole wrapper.
48
128
  content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
49
129
  structuredContent: payload,
50
- ...(payload['ok'] === false ? { isError: false } : {}),
130
+ ...(payload.ok === false ? { isError: false } : {}),
51
131
  };
52
132
  }
53
- function failure(message) {
54
- 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
+ };
55
175
  }
56
- /** Runs a CLI command and shapes the result identically for every tool. */
57
- async function commandTool(args, directory) {
58
- const run = await runXeerCommand(args, { cwd: directory });
59
- return structured({
60
- 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, {
61
190
  exitCode: run.exitCode,
62
- ...run.envelope,
63
- ...(run.stderr ? { stderr: run.stderr } : {}),
191
+ envelope,
192
+ }, {
193
+ ok: envelope.ok,
194
+ ...(adapterDiagnostic ? {
195
+ error: { code: 'adapter_failure', message: adapterDiagnostic.message },
196
+ } : {}),
64
197
  });
65
198
  }
66
- async function withDirectory(directory, body) {
199
+ async function withDirectory(directory, action, body) {
200
+ let resolved;
67
201
  try {
68
- return await body(await resolveDirectory(directory));
202
+ resolved = await resolveDirectory(directory);
69
203
  }
70
204
  catch (error) {
71
205
  if (error instanceof XeerCliError)
72
- return failure(error.message);
206
+ return failure(action, 'path_refused', error.message);
73
207
  throw error;
74
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".`);
75
217
  }
76
- const CHECK_DESCRIPTION = [
77
- 'Validate a Xeer project and return its diagnostics as JSON. This is the repair loop:',
78
- 'read diagnostics, apply the smallest edit each one names, run it again, converge on ok: true.',
79
- 'Runs the manifest stage and, if that passes, the module-graph/zone/operations/type analysis.',
80
- 'Envelope: { protocol: "xeer.command.v0", command: "check", ok, diagnostics: Diagnostic[],',
81
- 'result?: { manifest } }. Diagnostic codes are stable — look one up with xeer_diagnostics.',
82
- ].join(' ');
83
- const BUILD_DESCRIPTION = [
84
- 'Build the content-addressed artifact. Runs check first, so build diagnostics are a superset',
85
- 'of check diagnostics plus bundling, budget, and asset codes (XE14xx, XE15xx).',
86
- 'On success result is { artifactId, outputDirectory, modules, assets, operations }.',
87
- 'Run xeer_test before this to prove the application behaves, not just that it compiles.',
88
- ].join(' ');
89
- export function createXeerMcpServer() {
218
+ export function createXeerMcpServer(options = {}) {
219
+ const profile = options.profile ?? 'author';
90
220
  const server = new McpServer({ name: 'xeer', version }, {
91
221
  instructions: [
92
222
  'Xeer is a constrained full-stack platform whose diagnostics are its agent surface.',
93
- `Project root for this server: ${projectRoot()}.`,
223
+ 'This server is confined to its configured project root.',
94
224
  'Loop: xeer_new to scaffold, xeer_check after every edit, xeer_test to prove behaviour,',
95
- 'xeer_build before deploy. A green check means it compiles; only a green test means it works.',
96
- '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',
97
230
  'and use xeer_diagnostics to learn what a code means and which edit closes it.',
98
231
  'Use xeer_dev_start/status/stop only when you need a running preview and inspector;',
99
232
  'a check-edit-check loop plus xeer_test is enough to build and repair a project.',
100
233
  ].join(' '),
101
234
  });
102
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)));
103
312
  server.registerTool('xeer_check', {
104
- title: 'Check a Xeer project',
105
- description: CHECK_DESCRIPTION,
106
- inputSchema: { directory: directoryArgument },
107
- outputSchema: envelopeOutput,
108
- annotations: { readOnlyHint: true, openWorldHint: false },
109
- }, 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)));
110
316
  server.registerTool('xeer_build', {
111
- title: 'Build a Xeer project',
112
- description: BUILD_DESCRIPTION,
113
- inputSchema: { directory: directoryArgument },
114
- outputSchema: envelopeOutput,
115
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
116
- }, 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)));
117
320
  server.registerTool('xeer_test', {
118
- title: 'Run a Xeer project\'s tests',
119
- description: [
120
- 'Run the application\'s own tests: build, type-check tests/**/*.test.ts against the generated',
121
- 'contract, boot the verified artifact in workerd with fresh isolated state, run every test, tear',
122
- 'down. This is the convergence check — a green check proves the project compiles, a green test',
123
- 'proves it behaves. Returns { ok, total, passed, failed, cases, diagnostics, events }.',
124
- 'Each failed case carries a Diagnostic-shaped failure with matcher, expected, actual, and the',
125
- 'project-relative test location: XE1904 assertion, XE1905 threw or unexpectedly refused call,',
126
- 'XE1906 timeout. Compiler diagnostics abort the run before any test boots.',
127
- 'Tests call as(\'alice\'|\'bob\'|\'guest\'), so ownership and authorization are directly testable.',
128
- ].join(' '),
129
- inputSchema: {
130
- directory: directoryArgument,
131
- timeoutMilliseconds: z.number().int().min(1_000).max(1_800_000).optional(),
132
- },
133
- outputSchema: {
134
- argv: z.array(z.string()),
321
+ ...toolPolicy('test'),
322
+ outputSchema: wrappedOutput(z.object({
135
323
  exitCode: z.number().nullable(),
136
324
  ok: z.boolean(),
137
325
  reason: z.string().optional(),
@@ -155,75 +343,80 @@ export function createXeerMcpServer() {
155
343
  type: z.string(),
156
344
  data: z.unknown(),
157
345
  }).loose()),
158
- stderr: z.string().optional(),
159
- },
160
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
161
- }, async ({ directory, timeoutMilliseconds }) => withDirectory(directory, async (resolved) => {
162
- const run = await runXeerTests({
163
- directory: resolved,
164
- ...(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
+ } : {}),
165
367
  });
166
- return structured(run);
167
368
  }));
168
369
  server.registerTool('xeer_new', {
169
- title: 'Scaffold a Xeer project',
170
- description: 'Create a new Xeer project in an empty or non-existent directory. '
171
- + 'The scaffold checks and builds clean, so use it as the starting point rather than writing '
172
- + 'a manifest by hand. result is { directory, name, files }.',
173
- inputSchema: {
174
- directory: z.string().describe('Target directory, relative to the server root. Must be empty or absent.'),
175
- },
176
- outputSchema: envelopeOutput,
177
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
178
- }, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['new', resolved], projectRoot())));
370
+ ...toolPolicy('new'),
371
+ outputSchema: commandOutput,
372
+ }, async ({ directory, template }) => withDirectory(directory, 'new', (resolved) => commandTool('new', ['new', resolved, ...(template === undefined ? [] : ['--template', template])], projectRoot())));
179
373
  server.registerTool('xeer_doctor', {
180
- title: 'Diagnose the Xeer toolchain',
181
- description: 'Check the local toolchain and generated-file state. Use it when a command fails for '
182
- + 'a reason no project edit explains. result is { checks, summary }; failures also appear as XE4001 '
183
- + 'diagnostics.',
184
- inputSchema: { directory: directoryArgument },
185
- outputSchema: envelopeOutput,
186
- annotations: { readOnlyHint: true, openWorldHint: false },
187
- }, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['doctor', resolved], resolved)));
188
- server.registerTool('xeer_deploy', {
189
- title: 'Deploy a Xeer project',
190
- description: 'Build, verify, and upload the artifact to the control plane. Requires a builder '
191
- + 'credential: XE5002 means a human must run `xeer auth login` first. result is '
192
- + '{ application, url, ... }.',
193
- inputSchema: {
194
- directory: directoryArgument,
195
- controlUrl: z.string().optional().describe('Control-plane origin override, e.g. https://control.example.com.'),
196
- },
197
- outputSchema: envelopeOutput,
198
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
199
- }, async ({ directory, controlUrl }) => withDirectory(directory, (resolved) => commandTool(['deploy', resolved, ...(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
+ }
200
401
  server.registerTool('xeer_auth_status', {
201
- title: 'Report builder sign-in status',
202
- description: 'Report whether a builder credential is available for deployment. Sign-in itself is '
203
- + 'interactive and cannot be completed by an agent; XE5002 means ask the human to run '
204
- + '`xeer auth login`.',
205
- inputSchema: {
206
- controlUrl: z.string().optional().describe('Control-plane origin override.'),
207
- },
208
- outputSchema: envelopeOutput,
209
- annotations: { readOnlyHint: true, openWorldHint: true },
210
- }, 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
+ });
211
410
  server.registerTool('xeer_inspect', {
212
- title: 'Inspect a running preview',
213
- description: 'Read the inspector API of a running dev or preview server: normalized manifest '
214
- + '(manifest), per-table record counts (state), the bounded structured log ring (logs), or '
215
- + 'every stored record with the schema that describes it (export). Use the URLs from '
216
- + 'xeer_dev_status.preview.',
217
- inputSchema: {
218
- previewUrl: z.string().describe('Preview URL from preview.ready, e.g. http://127.0.0.1:43127/.'),
219
- view: z.enum(['manifest', 'state', 'logs', 'export']).default('manifest'),
220
- after: z.string().optional().describe('Log cursor, for view: "logs".'),
221
- },
222
- outputSchema: envelopeOutput,
223
- annotations: { readOnlyHint: true, openWorldHint: true },
411
+ ...toolPolicy('inspect'),
412
+ outputSchema: commandOutput,
224
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);
225
418
  const command = view === 'manifest' ? 'inspect' : view;
226
- return commandTool([command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
419
+ return commandTool('inspect', [command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
227
420
  });
228
421
  const sessionOutput = {
229
422
  sessionId: z.string(),
@@ -243,7 +436,6 @@ export function createXeerMcpServer() {
243
436
  openDiagnostics: z.array(diagnosticSchema),
244
437
  exit: z.object({ code: z.number().optional(), reason: z.string().optional() }).loose().optional(),
245
438
  droppedEvents: z.number(),
246
- stderr: z.string().optional(),
247
439
  events: z.array(z.object({
248
440
  protocol: z.literal('xeer.dev.v0'),
249
441
  seq: z.number(),
@@ -252,24 +444,17 @@ export function createXeerMcpServer() {
252
444
  data: z.unknown(),
253
445
  }).loose()),
254
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
+ };
255
454
  server.registerTool('xeer_dev_start', {
256
- title: 'Start a dev session',
257
- description: [
258
- 'Start `xeer dev --json` and return the xeer.dev.v0 events it emitted up to the point it settled.',
259
- 'Returns when preview.ready arrives (status "ready"), or when the first compile fails',
260
- '(status "compile_failed", openDiagnostics populated). Port 0 by default, so the real URL is in',
261
- 'preview.ready. Keep the returned cursor and pass it to xeer_dev_status after each edit.',
262
- 'You do not need a dev session to repair diagnostics: xeer_check is the same compiler.',
263
- ].join(' '),
264
- inputSchema: {
265
- directory: directoryArgument,
266
- host: z.string().optional().describe('Defaults to 127.0.0.1.'),
267
- port: z.number().int().min(0).max(65535).optional().describe('Defaults to 0 (OS-assigned).'),
268
- timeoutMilliseconds: z.number().int().min(1_000).max(600_000).optional(),
269
- },
270
- outputSchema: sessionOutput,
271
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
272
- }, 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) => {
273
458
  let session;
274
459
  try {
275
460
  session = devSessions.start({
@@ -279,71 +464,78 @@ export function createXeerMcpServer() {
279
464
  });
280
465
  }
281
466
  catch (error) {
282
- 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));
283
470
  }
284
- await session.waitForStart(timeoutMilliseconds ?? 180_000);
285
- return structured(session.read(0));
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
+ });
491
+ }
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
+ });
286
497
  }));
287
498
  server.registerTool('xeer_dev_status', {
288
- title: 'Read new dev-session events',
289
- description: [
290
- 'Read the xeer.dev.v0 events emitted since `cursor`, then return the session summary.',
291
- 'After editing a file, call this with waitMilliseconds set: the CLI coalesces changes behind a',
292
- '75 ms quiet window, rebuilds, and emits compile.start, any compile.diagnostic, and compile.ready',
293
- 'when the rebuild is accepted. openDiagnostics is the outcome of the latest attempt, so an empty',
294
- 'openDiagnostics with a higher generation means the edit was accepted.',
295
- ].join(' '),
296
- inputSchema: {
297
- sessionId: z.string().optional().describe('Optional when only one session exists.'),
298
- cursor: z.number().int().min(0).default(0).describe('Last seq you have already read.'),
299
- waitMilliseconds: z.number().int().min(0).max(600_000).default(0)
300
- .describe('Wait up to this long for new events to arrive and settle.'),
301
- limit: z.number().int().min(1).max(1_000).default(200),
302
- },
303
- outputSchema: sessionOutput,
304
- annotations: { readOnlyHint: true, openWorldHint: false },
499
+ ...toolPolicy('dev.status'),
500
+ outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
305
501
  }, async ({ sessionId, cursor, waitMilliseconds, limit }) => {
306
502
  let session;
307
503
  try {
308
504
  session = devSessions.get(sessionId);
309
505
  }
310
506
  catch (error) {
311
- return failure(error instanceof Error ? error.message : String(error));
507
+ return failure('dev.status', 'session_refused', error instanceof Error ? error.message : String(error));
508
+ }
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.');
312
514
  }
313
- await session.waitForActivity(cursor, waitMilliseconds);
314
- return structured(session.read(cursor, limit));
515
+ return structured('dev.status', reusableSession(session.read(cursor, limit)));
315
516
  });
316
517
  server.registerTool('xeer_dev_stop', {
317
- title: 'Stop a dev session',
318
- description: 'Stop a dev session over the documented xeer.dev.control.v0 IPC shutdown, releasing '
319
- + 'the local state lease. Always stop sessions you started: the lease is per project and mode, '
320
- + 'so a leaked session blocks the next dev run with XE1812.',
321
- inputSchema: {
322
- sessionId: z.string().optional().describe('Optional when only one session exists.'),
323
- cursor: z.number().int().min(0).default(0),
324
- },
325
- outputSchema: sessionOutput,
326
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
518
+ ...toolPolicy('dev.stop'),
519
+ outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
327
520
  }, async ({ sessionId, cursor }) => {
328
521
  let session;
329
522
  try {
330
523
  session = devSessions.get(sessionId);
331
524
  }
332
525
  catch (error) {
333
- 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();
334
530
  }
335
- await session.stop();
336
- 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)));
337
535
  });
338
536
  server.registerTool('xeer_diagnostics', {
339
- title: 'Look up Xeer diagnostic codes',
340
- description: 'Explain a diagnostic code: what the platform observed and which edit closes it. '
341
- + 'Omit `code` for the whole generated reference. This is the same catalogue the compiler emits '
342
- + 'from, so it cannot drift from the codes you receive.',
343
- inputSchema: {
344
- code: z.string().optional().describe('An XE#### code, e.g. XE1202.'),
345
- },
346
- outputSchema: {
537
+ ...toolPolicy('diagnostics'),
538
+ outputSchema: wrappedOutput(z.object({
347
539
  code: z.string().optional(),
348
540
  family: z.string().optional(),
349
541
  means: z.string().optional(),
@@ -351,20 +543,19 @@ export function createXeerMcpServer() {
351
543
  surfaces: z.array(z.string()).optional(),
352
544
  codes: z.array(z.string()).optional(),
353
545
  reference: z.string().optional(),
354
- },
355
- annotations: { readOnlyHint: true, openWorldHint: false },
546
+ }).loose()),
356
547
  }, async ({ code }) => {
357
548
  if (!code) {
358
- return structured({ codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
549
+ return structured('diagnostics', { codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
359
550
  }
360
551
  const normalized = code.trim().toUpperCase();
361
552
  const definition = diagnosticDefinition(normalized);
362
553
  if (!definition) {
363
554
  const known = Object.keys(DIAGNOSTICS).sort().join(', ');
364
- return failure(`Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
555
+ return failure('diagnostics', 'unknown_diagnostic', `Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
365
556
  }
366
557
  const family = DIAGNOSTIC_FAMILIES.find((candidate) => candidate.prefix === definition.prefix);
367
- return structured({
558
+ return structured('diagnostics', {
368
559
  code: definition.code,
369
560
  family: family ? `${family.title}: ${family.summary}` : definition.prefix,
370
561
  means: definition.means,
@@ -372,5 +563,5 @@ export function createXeerMcpServer() {
372
563
  surfaces: [...definition.surfaces],
373
564
  });
374
565
  });
375
- return { server, devSessions };
566
+ return { server, devSessions, profile };
376
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.2",
3
+ "version": "0.2.4",
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",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/impetik/xeer.git",
10
10
  "directory": "packages/mcp"
11
11
  },
12
- "homepage": "https://github.com/impetik/xeer#readme",
12
+ "homepage": "https://xeer.run",
13
13
  "bugs": "https://github.com/impetik/xeer/issues",
14
14
  "keywords": [
15
15
  "xeer",
@@ -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.2",
47
- "@impetik/xeer-spec": "0.2.2"
46
+ "@impetik/xeer": "0.2.4",
47
+ "@impetik/xeer-spec": "0.2.4"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^24.1.0"