@octane-xplat/mcp 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +9 -5
  2. package/dist/_virtual/_rolldown/runtime.js +27 -0
  3. package/dist/actions/bridge.js +1 -0
  4. package/dist/context.js +0 -0
  5. package/dist/contracts/actions.js +1 -0
  6. package/dist/contracts/boundaries.js +1 -0
  7. package/dist/contracts/capabilities.js +1 -0
  8. package/dist/contracts/code.js +1 -0
  9. package/dist/contracts/errors.js +8 -0
  10. package/dist/contracts/evidence.js +8 -0
  11. package/dist/contracts/identity.js +8 -0
  12. package/dist/contracts/index.js +1 -0
  13. package/dist/contracts/inspection.js +1 -0
  14. package/dist/contracts/policy.js +8 -0
  15. package/dist/contracts/sessions.js +1 -0
  16. package/dist/contracts/status.js +1 -0
  17. package/dist/discovery.js +8 -0
  18. package/dist/docs.js +63 -0
  19. package/dist/inspection/service.js +3 -0
  20. package/dist/inspection/tools.js +43 -0
  21. package/dist/runtime/logs.js +1 -0
  22. package/dist/runtime/process.js +1 -0
  23. package/dist/runtime/service.js +1 -0
  24. package/dist/runtime/targets.js +1 -0
  25. package/dist/runtime/tools.js +119 -0
  26. package/dist/server.js +324 -0
  27. package/dist/status.js +8 -0
  28. package/dist/xplat-mcp.js +68 -0
  29. package/package.json +18 -12
  30. package/src/context.ts +108 -0
  31. package/src/{docs.mjs → docs.ts} +31 -26
  32. package/src/inspection/{service.mjs → service.ts} +1 -1
  33. package/src/inspection/{tools.mjs → tools.ts} +13 -5
  34. package/src/runtime/{tools.mjs → tools.ts} +16 -6
  35. package/src/{server.mjs → server.ts} +48 -33
  36. package/src/{xplat-mcp.mjs → xplat-mcp.ts} +21 -10
  37. /package/src/actions/{bridge.mjs → bridge.ts} +0 -0
  38. /package/src/contracts/{actions.mjs → actions.ts} +0 -0
  39. /package/src/contracts/{boundaries.mjs → boundaries.ts} +0 -0
  40. /package/src/contracts/{capabilities.mjs → capabilities.ts} +0 -0
  41. /package/src/contracts/{code.mjs → code.ts} +0 -0
  42. /package/src/contracts/{errors.mjs → errors.ts} +0 -0
  43. /package/src/contracts/{evidence.mjs → evidence.ts} +0 -0
  44. /package/src/contracts/{identity.mjs → identity.ts} +0 -0
  45. /package/src/contracts/{index.mjs → index.ts} +0 -0
  46. /package/src/contracts/{inspection.mjs → inspection.ts} +0 -0
  47. /package/src/contracts/{policy.mjs → policy.ts} +0 -0
  48. /package/src/contracts/{sessions.mjs → sessions.ts} +0 -0
  49. /package/src/contracts/{status.mjs → status.ts} +0 -0
  50. /package/src/{discovery.mjs → discovery.ts} +0 -0
  51. /package/src/runtime/{logs.mjs → logs.ts} +0 -0
  52. /package/src/runtime/{process.mjs → process.ts} +0 -0
  53. /package/src/runtime/{service.mjs → service.ts} +0 -0
  54. /package/src/runtime/{targets.mjs → targets.ts} +0 -0
  55. /package/src/{status.mjs → status.ts} +0 -0
  56. /package/{src → types}/contracts/actions.d.ts +0 -0
  57. /package/{src → types}/contracts/boundaries.d.ts +0 -0
  58. /package/{src → types}/contracts/capabilities.d.ts +0 -0
  59. /package/{src → types}/contracts/code.d.ts +0 -0
  60. /package/{src → types}/contracts/errors.d.ts +0 -0
  61. /package/{src → types}/contracts/evidence.d.ts +0 -0
  62. /package/{src → types}/contracts/identity.d.ts +0 -0
  63. /package/{src → types}/contracts/index.d.ts +0 -0
  64. /package/{src → types}/contracts/inspection.d.ts +0 -0
  65. /package/{src → types}/contracts/policy.d.ts +0 -0
  66. /package/{src → types}/contracts/sessions.d.ts +0 -0
  67. /package/{src → types}/contracts/status.d.ts +0 -0
package/dist/server.js ADDED
@@ -0,0 +1,324 @@
1
+ import { discovery_exports } from "./discovery.js";
2
+ import { errors_exports } from "./contracts/errors.js";
3
+ import { identity_exports } from "./contracts/identity.js";
4
+ import { status_exports } from "./status.js";
5
+ import { registerDocsTools } from "./docs.js";
6
+ import { evidence_exports } from "./contracts/evidence.js";
7
+ import { policy_exports } from "./contracts/policy.js";
8
+ import { registerRuntimeTools } from "./runtime/tools.js";
9
+ import { registerInspectionTools } from "./inspection/tools.js";
10
+ import "./inspection/service.js";
11
+ import { z } from "zod";
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { readFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ //#region src/server.ts
17
+ var SERVER_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")).version;
18
+ var INSTRUCTIONS = "MCP server for Octane Xplat apps. Call xplat_project_status first — it reports detected apps, framework versions, guidance, prerequisites, and capabilities. If several apps are detected, ask the developer which to use and bind it with xplat_select_app; never guess. With a dev-mode app running, xplat_dev_actions lists the app-declared action registry and xplat_dev_action_invoke runs one against an exact instance and registry revision. For runtime work, check xplat_capability_matrix then xplat_targets; xplat_launch starts an owned session as a job and xplat_attach binds a session you already run. Results always say what was actually checked; a successful build is not proof of behavior.";
19
+ var respond = (outcome) => ({
20
+ structuredContent: outcome,
21
+ isError: outcome.ok === false,
22
+ content: [{
23
+ type: "text",
24
+ text: JSON.stringify(outcome, null, 2)
25
+ }]
26
+ });
27
+ var ok = (data) => respond({
28
+ ok: true,
29
+ data
30
+ });
31
+ var fail = (error) => respond({
32
+ ok: false,
33
+ error
34
+ });
35
+ /** The bound app for runtime tools — explicit, configured, or sole-app. */
36
+ var boundApp = (context) => {
37
+ const selection = (0, status_exports.resolveSelection)(context);
38
+ return selection.state === "bound" ? selection.app : null;
39
+ };
40
+ var NO_APP = () => (0, errors_exports.contractError)("invalid_selection", "No app is bound.", { recovery: { action: "Bind an app with xplat_select_app first." } });
41
+ function createXplatMcpServer(context) {
42
+ const server = new McpServer({
43
+ name: status_exports.SERVER_NAME,
44
+ version: SERVER_VERSION
45
+ }, { instructions: INSTRUCTIONS });
46
+ context.policy.setElicitor(async (scope, reason) => {
47
+ try {
48
+ if (!server.server.getClientCapabilities()?.elicitation) return "denied";
49
+ const answer = await server.server.elicitInput({
50
+ message: `Octane Xplat MCP requests permission "${scope}": ${reason}`,
51
+ requestedSchema: {
52
+ type: "object",
53
+ properties: { grant: {
54
+ type: "boolean",
55
+ description: "Grant this permission for the current task"
56
+ } },
57
+ required: ["grant"]
58
+ }
59
+ });
60
+ return answer.action === "accept" && answer.content?.grant === true ? "granted" : "denied";
61
+ } catch {
62
+ return "denied";
63
+ }
64
+ });
65
+ if (context.code) for (const platform of identity_exports.PLATFORMS) context.capabilities.register({
66
+ platform,
67
+ describe: () => [{
68
+ capability: "code_search",
69
+ status: "supported",
70
+ detail: "Exact search reads the bound app and its installed framework sources directly. An optional local semantic index accelerates intent-style queries and never blocks exact retrieval."
71
+ }]
72
+ });
73
+ server.registerTool("xplat_project_status", {
74
+ description: "Report detected project identity, framework versions, guidance, docs/code-index status, declared platforms, runtime prerequisites, and the capability summary. Call first; call again after changing workspaces or installing dependencies.",
75
+ inputSchema: { refresh: z.boolean().optional().describe("Re-scan the workspace instead of using the startup snapshot") }
76
+ }, async ({ refresh }) => {
77
+ if (refresh) {
78
+ context.discovery = (0, discovery_exports.discoverWorkspace)(context.workspaceRoot);
79
+ const bound = context.boundApp;
80
+ if (bound) {
81
+ const still = context.discovery.apps.find((a) => a.id === bound.id);
82
+ context.boundApp = still ?? null;
83
+ context.selectionReason = still ? "explicit" : null;
84
+ }
85
+ }
86
+ return ok(await (0, status_exports.buildProjectStatus)(context));
87
+ });
88
+ server.registerTool("xplat_select_app", {
89
+ description: "Bind an explicit app for subsequent work. Required when several apps are detected — the server never picks silently. Pass the app id, relative path, or package name from the status report.",
90
+ inputSchema: { app: z.string().describe("App id, workspace-relative path, or package name") }
91
+ }, async ({ app }) => {
92
+ const result = (0, status_exports.bindApp)(context, app);
93
+ if (result.error) {
94
+ if (result.error === "ambiguous_selection") return fail((0, errors_exports.contractError)("ambiguous_selection", `"${app}" matches several apps.`, {
95
+ recovery: { action: "Ask the developer which app to use, then pass its id." },
96
+ context: { candidates: result.candidates }
97
+ }));
98
+ return fail((0, errors_exports.contractError)("invalid_selection", `No app matches "${app}".`, {
99
+ recovery: { action: "Pick an id from candidates, or check the workspace root." },
100
+ context: { candidates: result.candidates }
101
+ }));
102
+ }
103
+ return ok({
104
+ bound: result.app,
105
+ reason: "explicit"
106
+ });
107
+ });
108
+ server.registerTool("xplat_capability_matrix", {
109
+ description: "Per-platform capability matrix: build, launch, logs, UI inspection, screenshot, and interaction are separate capabilities. Unsupported or missing capabilities come with an explanation and a usable alternative when one exists.",
110
+ inputSchema: { platform: z.enum(identity_exports.PLATFORMS).optional().describe("Limit to one platform") }
111
+ }, async ({ platform }) => {
112
+ const status = await (0, status_exports.buildProjectStatus)(context);
113
+ return ok({ capabilities: platform ? status.capabilities.filter((m) => m.platform === platform) : status.capabilities });
114
+ });
115
+ server.registerTool("xplat_permissions", {
116
+ description: "Report permission grants and their states. Workspace scope and ordinary runtime actions reuse the task authorization; screenshots require an explicit per-task developer grant checked at action time.",
117
+ inputSchema: { scope: z.enum(policy_exports.PERMISSION_SCOPES).optional() }
118
+ }, async ({ scope }) => {
119
+ const grants = context.policy.list();
120
+ if (!scope) return ok({ grants });
121
+ const grant = grants.find((g) => g.scope === scope);
122
+ const check = context.policy.require(scope);
123
+ return ok({
124
+ grant,
125
+ check: check.granted ? { granted: true } : {
126
+ granted: false,
127
+ error: check.error
128
+ }
129
+ });
130
+ });
131
+ server.registerTool("xplat_sessions", {
132
+ description: "List bound sessions: owned sessions the server started and attached user-owned sessions. Runtime tools bind one app to one target; attached sessions are preserved on end.",
133
+ inputSchema: {}
134
+ }, async () => ok({ sessions: context.sessions.list() }));
135
+ server.registerTool("xplat_job_status", {
136
+ description: "Bounded status query for a job started by a runtime tool.",
137
+ inputSchema: { jobId: z.string() }
138
+ }, async ({ jobId }) => {
139
+ const job = context.jobs.status(jobId);
140
+ return job ? ok({ job }) : fail((0, errors_exports.contractError)("not_found", `No job "${jobId}".`, { recovery: { action: "Start a job first; finished history is bounded." } }));
141
+ });
142
+ server.registerTool("xplat_job_cancel", {
143
+ description: "Cooperatively cancel a job. Cancelling signals only that job — it never kills an unrelated app or another user’s process.",
144
+ inputSchema: { jobId: z.string() }
145
+ }, async ({ jobId }) => {
146
+ const job = context.jobs.cancel(jobId);
147
+ return job ? ok({ job }) : fail((0, errors_exports.contractError)("not_found", `No job "${jobId}".`));
148
+ });
149
+ server.registerTool("xplat_session_end", {
150
+ description: "End a session with ownership-safe cleanup: owned sessions release their resources; attached user-owned sessions are detached and preserved.",
151
+ inputSchema: { sessionId: z.string() }
152
+ }, async ({ sessionId }) => {
153
+ const usable = context.sessions.requireUsable(sessionId);
154
+ if (!usable.usable && usable.error.code === "not_found") return fail(usable.error);
155
+ return ok({ result: await context.runtime.endSession(sessionId) });
156
+ });
157
+ server.registerTool("xplat_dev_actions", {
158
+ description: "List the bound app's declared development actions — the live registry announced by a running dev app: stable ids, titles, input/result contracts, effects, availability with reasons, and the registry revision. Never executes an action. An empty registry is reported distinctly from a disconnected app.",
159
+ inputSchema: {}
160
+ }, async () => {
161
+ const app = boundApp(context);
162
+ return app ? ok(await context.devActions.list(app)) : fail(NO_APP());
163
+ });
164
+ server.registerTool("xplat_dev_action_invoke", {
165
+ description: "Invoke an app-declared development action against a specific app instance and registry revision. Input is validated against the declared contract before anything runs; stale revisions, unavailable actions and saturated queues are rejected up front. Returns a job — poll xplat_job_status, cancel with xplat_job_cancel. Successful setup does not prove app behavior; verify separately.",
166
+ inputSchema: {
167
+ appInstanceId: z.string().optional().describe("Target app instance from xplat_dev_actions; required when several instances are live"),
168
+ actionId: z.string().describe("Stable action id from the registry"),
169
+ input: z.record(z.string(), z.unknown()).optional().describe("Named inputs matching the declared input contract"),
170
+ registryRevision: z.number().optional().describe("The revision returned by xplat_dev_actions — stale revisions are rejected"),
171
+ requestKey: z.string().optional().describe("Idempotence key: a retry with the same key returns the live job instead of re-running")
172
+ }
173
+ }, async ({ appInstanceId, actionId, input, registryRevision, requestKey }) => {
174
+ const app = boundApp(context);
175
+ if (!app) return fail(NO_APP());
176
+ const result = await context.devActions.invoke(app, {
177
+ appInstanceId,
178
+ actionId,
179
+ input: input ?? {},
180
+ registryRevision,
181
+ requestKey
182
+ });
183
+ return result.error ? fail(result.error) : ok(result);
184
+ });
185
+ server.registerTool("xplat_dev_action_status", {
186
+ description: "Status of a development-action invocation: job state, structured result or failure, and disclosed partial effects. Same data as xplat_job_status, restricted to dev-action jobs.",
187
+ inputSchema: { jobId: z.string() }
188
+ }, async ({ jobId }) => {
189
+ const described = context.devActions.describeJob(jobId);
190
+ if (!described) return fail((0, errors_exports.contractError)("not_found", `No job "${jobId}".`, { recovery: { action: "Invoke an action first; finished history is bounded." } }));
191
+ if (described.job.kind !== "dev-action") return fail((0, errors_exports.contractError)("invalid_selection", `Job "${jobId}" is a ${described.job.kind} job, not a dev-action invocation.`, { recovery: { action: "Use xplat_job_status for other job kinds." } }));
192
+ return ok(described);
193
+ });
194
+ server.registerTool("xplat_evidence", {
195
+ description: "List, retain, or delete evidence artifacts. Evidence is session-scoped and deleted when the MCP session ends; retain needs an explicit expiration or manual deletion. Deletion cannot erase an external client transcript.",
196
+ inputSchema: {
197
+ op: z.enum([
198
+ "list",
199
+ "retain",
200
+ "delete"
201
+ ]),
202
+ id: z.string().optional().describe("Evidence record id (required for retain/delete)"),
203
+ expiresAt: z.string().optional().describe("ISO expiration for retain; omit for manual deletion only"),
204
+ kind: z.enum(evidence_exports.EVIDENCE_KINDS).optional().describe("Filter for list")
205
+ }
206
+ }, async ({ op, id, expiresAt, kind }) => {
207
+ if (op === "list") return ok({ evidence: context.evidence.list({ kind }) });
208
+ if (!id) return fail((0, errors_exports.contractError)("invalid_selection", `op "${op}" requires an evidence id.`));
209
+ if (op === "retain") {
210
+ const result = context.evidence.retain(id, { expiresAt: expiresAt ?? null });
211
+ return "error" in result ? fail(result.error) : ok(result);
212
+ }
213
+ const result = context.evidence.delete(id);
214
+ return "error" in result ? fail(result.error) : ok(result);
215
+ });
216
+ const boundAppForCode = () => {
217
+ const selection = (0, status_exports.resolveSelection)(context);
218
+ if (selection.state === "ambiguous") return {
219
+ app: null,
220
+ error: (0, errors_exports.contractError)("ambiguous_selection", "Several apps are detected — code retrieval targets the bound app and its installed packages, never a guessed one.", {
221
+ recovery: { action: "Ask the developer which app to use, then call xplat_select_app." },
222
+ context: { candidates: selection.candidates.map((a) => a.id) }
223
+ })
224
+ };
225
+ return { app: selection.state === "bound" ? selection.app : null };
226
+ };
227
+ const noCodeProvider = () => fail((0, errors_exports.contractError)("unavailable", "No code-search provider is wired into this server (work item C integration absent).", { recovery: { action: "Use your normal file tools; report the missing provider to the developer." } }));
228
+ const codeResult = (result) => {
229
+ if (!result?.status) return ok(result);
230
+ const code = result.status === "unavailable" ? "unavailable" : "invalid_selection";
231
+ return fail((0, errors_exports.contractError)(code, result.detail ?? result.reason ?? result.status, { context: {
232
+ status: result.status,
233
+ reason: result.reason,
234
+ corpus: result.corpus,
235
+ path: result.path
236
+ } }));
237
+ };
238
+ server.registerTool("xplat_code_search", {
239
+ description: "Search current source. mode=exact (default auto includes it) finds identifiers/tokens by exact match over live files; mode=semantic uses an optional local index that never blocks exact retrieval. Corpora stay labeled: app = the bound app, framework = its installed @octane-xplat packages. Results are leads — open refs with xplat_code_read before citing an API.",
240
+ inputSchema: {
241
+ query: z.string().describe("Identifier, phrase, or intent to search for"),
242
+ mode: z.enum([
243
+ "auto",
244
+ "exact",
245
+ "semantic"
246
+ ]).optional().describe("exact = token search over current files; semantic = local index; auto = both"),
247
+ corpus: z.enum([
248
+ "app",
249
+ "framework",
250
+ "all"
251
+ ]).optional().describe("Default: all"),
252
+ platform: z.enum(identity_exports.PLATFORMS).optional().describe("Include shared files plus files marked for this platform"),
253
+ package: z.string().optional().describe("Limit the framework corpus to one package"),
254
+ limit: z.number().int().min(1).max(50).optional()
255
+ }
256
+ }, async ({ query, mode, corpus, platform, package: pkg, limit }) => {
257
+ if (!context.code) return noCodeProvider();
258
+ const check = context.policy.require("workspace");
259
+ if (!check.granted) return fail(check.error);
260
+ const { app, error } = boundAppForCode();
261
+ if (error) return fail(error);
262
+ try {
263
+ return codeResult(await context.code.search({
264
+ app,
265
+ workspaceKind: context.discovery.workspace.kind,
266
+ query,
267
+ mode,
268
+ corpus,
269
+ platform,
270
+ package: pkg,
271
+ limit
272
+ }));
273
+ } catch (value) {
274
+ return fail((0, errors_exports.toContractError)(value));
275
+ }
276
+ });
277
+ server.registerTool("xplat_code_read", {
278
+ description: "Read current source by a search ref (xplat-code://…) or explicit corpus + path. Bounded slices return a continuation offset; a file that changed or vanished is reported, never served stale.",
279
+ inputSchema: {
280
+ ref: z.string().optional().describe("xplat-code://<corpus>/<key> from a search hit"),
281
+ corpus: z.enum(["app", "framework"]).optional(),
282
+ package: z.string().optional().describe("Framework package name for corpus=framework"),
283
+ path: z.string().optional().describe("Workspace-relative path (app) or path inside the package (framework)"),
284
+ offset: z.number().int().min(0).optional(),
285
+ limit: z.number().int().min(1).max(64e3).optional()
286
+ }
287
+ }, async ({ ref, corpus, package: pkg, path, offset, limit }) => {
288
+ if (!context.code) return noCodeProvider();
289
+ const check = context.policy.require("workspace");
290
+ if (!check.granted) return fail(check.error);
291
+ const { app, error } = boundAppForCode();
292
+ if (error) return fail(error);
293
+ try {
294
+ return codeResult(await context.code.read({
295
+ app,
296
+ workspaceKind: context.discovery.workspace.kind,
297
+ ref,
298
+ corpus,
299
+ package: pkg,
300
+ path,
301
+ offset,
302
+ limit
303
+ }));
304
+ } catch (value) {
305
+ return fail((0, errors_exports.toContractError)(value));
306
+ }
307
+ });
308
+ registerRuntimeTools(server, context, respond);
309
+ registerDocsTools(server, context, respond);
310
+ registerInspectionTools(server, context, respond);
311
+ return {
312
+ server,
313
+ context,
314
+ /** Ownership-safe teardown: end owned sessions, delete session-scoped evidence. */
315
+ async shutdown() {
316
+ return {
317
+ sessions: await context.runtime.endAll(),
318
+ evidence: context.evidence.closeSession()
319
+ };
320
+ }
321
+ };
322
+ }
323
+ //#endregion
324
+ export { createXplatMcpServer };
package/dist/status.js ADDED
@@ -0,0 +1,8 @@
1
+ import { __exportAll, __reExport } from "./_virtual/_rolldown/runtime.js";
2
+ export * from "@octane-xplat/cli/mcp/status";
3
+ //#region src/status.ts
4
+ var status_exports = /* @__PURE__ */ __exportAll({});
5
+ import * as import__octane_xplat_cli_mcp_status from "@octane-xplat/cli/mcp/status";
6
+ __reExport(status_exports, import__octane_xplat_cli_mcp_status);
7
+ //#endregion
8
+ export { status_exports };
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { status_exports } from "./status.js";
3
+ import { policy_exports } from "./contracts/policy.js";
4
+ import { createXplatMcpServer } from "./server.js";
5
+ import { readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { createCodeService } from "@octane-xplat/cli/mcp/code-service";
10
+ //#region src/xplat-mcp.ts
11
+ var usage = `xplat-mcp — Octane Xplat MCP server (stdio)
12
+
13
+ Usage: xplat-mcp [--workspace <path>] [--app <id|path|name>]
14
+ [--permissions <scope,scope>] [--evidence-dir <path>]
15
+
16
+ Options may also come from the environment:
17
+ XPLAT_MCP_WORKSPACE, XPLAT_MCP_APP, XPLAT_MCP_PERMISSIONS, XPLAT_MCP_EVIDENCE_DIR
18
+
19
+ Scopes: ${policy_exports.PERMISSION_SCOPES.join(", ")}
20
+ Workspace reads and ordinary runtime actions reuse the developer's task
21
+ authorization; visual-capture always needs an explicit per-task grant.
22
+ `;
23
+ var arg = (name) => {
24
+ const index = process.argv.indexOf(`--${name}`);
25
+ return index >= 0 ? process.argv[index + 1] : void 0;
26
+ };
27
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
28
+ process.stderr.write(usage);
29
+ process.exit(0);
30
+ }
31
+ var permissionScopes = (arg("permissions") ?? process.env.XPLAT_MCP_PERMISSIONS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
32
+ var unknown = permissionScopes.filter((s) => !policy_exports.PERMISSION_SCOPES.includes(s));
33
+ if (unknown.length) process.stderr.write(`xplat-mcp: ignoring unknown permission scopes: ${unknown.join(", ")}\n`);
34
+ var workspaceRoot = arg("workspace") ?? process.env.XPLAT_MCP_WORKSPACE ?? process.cwd();
35
+ var manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8"));
36
+ var context = (0, status_exports.createWorkspaceContext)({
37
+ workspaceRoot,
38
+ serverIdentity: {
39
+ name: "@octane-xplat/mcp",
40
+ version: manifest.version
41
+ },
42
+ appSelector: arg("app") ?? process.env.XPLAT_MCP_APP,
43
+ permissionScopes: permissionScopes.filter((s) => policy_exports.PERMISSION_SCOPES.includes(s)),
44
+ evidenceRoot: arg("evidence-dir") ?? process.env.XPLAT_MCP_EVIDENCE_DIR,
45
+ code: createCodeService({
46
+ workspaceRoot,
47
+ workspaceKind: () => context.discovery.workspace.kind
48
+ })
49
+ });
50
+ var { server, shutdown } = createXplatMcpServer(context);
51
+ var closing = false;
52
+ var close = async () => {
53
+ if (closing) return;
54
+ closing = true;
55
+ try {
56
+ await shutdown();
57
+ } catch (error) {
58
+ const detail = error instanceof Error ? error.message : String(error);
59
+ process.stderr.write(`xplat-mcp: shutdown cleanup failed: ${detail}\n`);
60
+ } finally {
61
+ await server.close();
62
+ process.exit(0);
63
+ }
64
+ };
65
+ process.on("SIGINT", close);
66
+ process.on("SIGTERM", close);
67
+ await server.connect(new StdioServerTransport());
68
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "MCP server for Octane Xplat consumer apps — project identity, capability discovery, permission and evidence contracts.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,39 +9,45 @@
9
9
  "directory": "packages/mcp"
10
10
  },
11
11
  "bin": {
12
- "xplat-mcp": "./src/xplat-mcp.mjs"
12
+ "xplat-mcp": "./dist/xplat-mcp.js"
13
13
  },
14
14
  "files": [
15
15
  "src",
16
+ "types",
17
+ "dist",
16
18
  "README.md"
17
19
  ],
18
20
  "type": "module",
19
21
  "exports": {
20
22
  "./contracts": {
21
- "types": "./src/contracts/index.d.ts",
22
- "default": "./src/contracts/index.mjs"
23
+ "types": "./types/contracts/index.d.ts",
24
+ "default": "./dist/contracts/index.js"
23
25
  },
24
26
  "./contracts/*": {
25
- "types": "./src/contracts/*.d.ts",
26
- "default": "./src/contracts/*.mjs"
27
- }
27
+ "types": "./types/contracts/*.d.ts",
28
+ "default": "./dist/contracts/*.js"
29
+ },
30
+ "./package.json": "./package.json"
28
31
  },
29
32
  "publishConfig": {
30
33
  "access": "public"
31
34
  },
32
35
  "dependencies": {
33
36
  "@modelcontextprotocol/sdk": "1.31.0",
34
- "@octane-xplat/cli": "0.15.0",
37
+ "@octane-xplat/cli": "0.16.1",
35
38
  "yaml": "2.9.1",
36
39
  "zod": "4.6.5"
37
40
  },
38
41
  "devDependencies": {
39
- "typescript": "5.9.3"
42
+ "@types/node": "24.10.1",
43
+ "typescript": "5.9.3",
44
+ "vite": "8.3.0"
40
45
  },
41
46
  "scripts": {
42
- "start": "node src/xplat-mcp.mjs",
43
- "test": "node --test tests/*.test.mjs",
44
- "test:consumer": "node tests/consumer-workflow.mjs",
47
+ "start": "node src/xplat-mcp.ts",
48
+ "build": "vite build",
49
+ "test": "pnpm build && node --test tests/*.test.mjs",
50
+ "test:consumer": "pnpm build && node tests/consumer-workflow.mjs",
45
51
  "typecheck": "tsc -p tsconfig.json"
46
52
  }
47
53
  }
package/src/context.ts ADDED
@@ -0,0 +1,108 @@
1
+ // Shared internal types for the MCP transport adapter. The context object is
2
+ // assembled by createWorkspaceContext in the shared service layer
3
+ // (@octane-xplat/cli/mcp/status); its registries/stores are typed by the
4
+ // published contracts while service slots (runtime, docs, inspection,
5
+ // dev-actions) keep the adapter-facing surface the tools actually use.
6
+
7
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
8
+ import type { discoverWorkspace } from '@octane-xplat/cli/mcp/discovery'
9
+ import type { CapabilityRegistry } from '../types/contracts/capabilities'
10
+ import type { CodeProvider } from '../types/contracts/code'
11
+ import type { ToolOutcome } from '../types/contracts/errors'
12
+ import type { EvidenceStore } from '../types/contracts/evidence'
13
+ import type { AppRef, Platform } from '../types/contracts/identity'
14
+ import type { PolicyStore } from '../types/contracts/policy'
15
+ import type { Job, JobRegistry, SessionRegistry } from '../types/contracts/sessions'
16
+ import type { ServerInfo } from '../types/contracts/status'
17
+
18
+ export type Discovery = ReturnType<typeof discoverWorkspace>
19
+
20
+ /** The app-binding result: explicit/configured binding, sole app, or a list the developer must choose from. */
21
+ export type Selection =
22
+ | { state: 'bound'; app: AppRef; reason: string | null }
23
+ | { state: 'ambiguous'; candidates: AppRef[] }
24
+ | { state: 'none'; reason: string }
25
+
26
+ export type SelectionReason = 'explicit' | 'configured' | 'sole-app' | null
27
+
28
+ export interface RuntimeService {
29
+ registerCapabilities(): void
30
+ listTargets(args: { platform?: Platform }): Promise<any>
31
+ build(args: { platform: Platform; requestKey?: string }): Promise<any>
32
+ launch(args: {
33
+ platform: Platform
34
+ targetId?: string
35
+ requestKey?: string
36
+ browserMode?: 'headless' | 'headed'
37
+ }): Promise<any>
38
+ attach(args: { platform: Platform; targetId?: string }): Promise<any>
39
+ readLogs(args: {
40
+ sessionId?: string
41
+ jobId?: string
42
+ cursor?: number
43
+ limit?: number
44
+ stream?: 'stdout' | 'stderr' | 'lifecycle'
45
+ }): any
46
+ endSession(sessionId: string): Promise<any>
47
+ endAll(): Promise<any>
48
+ }
49
+
50
+ export interface DocsProvider {
51
+ status(app: AppRef | null): unknown
52
+ serviceFor(app: AppRef): {
53
+ search(args: any): Promise<any>
54
+ read(args: any): Promise<any>
55
+ guidance(args: any): Promise<any>
56
+ }
57
+ }
58
+
59
+ export interface InspectionService {
60
+ inspect(args: any): Promise<any>
61
+ interact(args: any): Promise<any>
62
+ screenshot(args: any): Promise<any>
63
+ }
64
+
65
+ export interface DevActionsService {
66
+ list(app: AppRef): Promise<any>
67
+ invoke(
68
+ app: AppRef,
69
+ args: {
70
+ appInstanceId?: string
71
+ actionId: string
72
+ input: Record<string, unknown>
73
+ registryRevision?: number
74
+ requestKey?: string
75
+ },
76
+ ): Promise<any>
77
+ describeJob(jobId: string): { job: Job; [key: string]: unknown } | null
78
+ }
79
+
80
+ export interface WorkspaceContext {
81
+ workspaceRoot: string
82
+ configuredAppSelector: string | null
83
+ serverIdentity: ServerInfo
84
+ discovery: Discovery
85
+ boundApp: AppRef | null
86
+ selectionReason: SelectionReason
87
+ capabilities: CapabilityRegistry
88
+ policy: PolicyStore
89
+ sessions: SessionRegistry
90
+ jobs: JobRegistry
91
+ evidence: EvidenceStore
92
+ docsStatus: (app: AppRef | null) => unknown
93
+ codeIndexStatus: (app: AppRef | null) => unknown
94
+ code: CodeProvider | null
95
+ docs: DocsProvider
96
+ devActions: DevActionsService
97
+ runtime: RuntimeService
98
+ inspection: InspectionService
99
+ }
100
+
101
+ /** The structuredContent envelope every tool result shares. */
102
+ export interface ToolResponse extends CallToolResult {
103
+ structuredContent: ToolOutcome<unknown>
104
+ isError: boolean
105
+ content: [{ type: 'text'; text: string }]
106
+ }
107
+
108
+ export type Respond = (outcome: ToolOutcome<unknown>) => ToolResponse
@@ -2,35 +2,40 @@
2
2
  // shared service layer at @octane-xplat/cli/mcp/docs-provider — the CLI's
3
3
  // `docs`/`guidance` commands project the same service.
4
4
  import { z } from 'zod'
5
- import { contractError, toContractError } from './contracts/errors.mjs'
6
- import { PLATFORMS } from './contracts/identity.mjs'
7
- import { resolveSelection } from './status.mjs'
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import { contractError, toContractError } from './contracts/errors.ts'
7
+ import { PLATFORMS } from './contracts/identity.ts'
8
+ import { resolveSelection } from './status.ts'
9
+ import type { Respond, Selection, WorkspaceContext } from './context.ts'
10
+ import type { Platform } from '../types/contracts/identity'
8
11
 
9
- export function registerDocsTools(server, context, respond) {
10
- const run = (method) => async (args) => {
11
- const permission = context.policy.require('workspace')
12
- if (!permission.granted) {
13
- return respond({ ok: false, error: permission.error })
14
- }
12
+ export function registerDocsTools(server: McpServer, context: WorkspaceContext, respond: Respond) {
13
+ const run =
14
+ (method: 'search' | 'read' | 'guidance') =>
15
+ async (args: any): Promise<ReturnType<Respond>> => {
16
+ const permission = context.policy.require('workspace')
17
+ if (!permission.granted) {
18
+ return respond({ ok: false, error: permission.error })
19
+ }
15
20
 
16
- const selection = resolveSelection(context)
17
- if (selection.state !== 'bound') {
18
- return respond({
19
- ok: false,
20
- error: contractError(
21
- selection.state === 'ambiguous' ? 'ambiguous_selection' : 'invalid_selection',
22
- 'Bind an app with xplat_select_app before retrieving docs.',
23
- ),
24
- })
25
- }
21
+ const selection = resolveSelection(context) as Selection
22
+ if (selection.state !== 'bound') {
23
+ return respond({
24
+ ok: false,
25
+ error: contractError(
26
+ selection.state === 'ambiguous' ? 'ambiguous_selection' : 'invalid_selection',
27
+ 'Bind an app with xplat_select_app before retrieving docs.',
28
+ ),
29
+ })
30
+ }
26
31
 
27
- try {
28
- const data = await context.docs.serviceFor(selection.app)[method](args)
29
- return respond({ ok: true, data })
30
- } catch (error) {
31
- return respond({ ok: false, error: toContractError(error) })
32
+ try {
33
+ const data = await context.docs.serviceFor(selection.app)[method](args)
34
+ return respond({ ok: true, data })
35
+ } catch (error) {
36
+ return respond({ ok: false, error: toContractError(error) })
37
+ }
32
38
  }
33
- }
34
39
 
35
40
  server.registerTool(
36
41
  'xplat_docs_search',
@@ -44,7 +49,7 @@ export function registerDocsTools(server, context, respond) {
44
49
  .string()
45
50
  .optional()
46
51
  .describe('Explicit comparison version, visibly outside the installed binding.'),
47
- platform: z.enum(PLATFORMS).optional(),
52
+ platform: z.enum(PLATFORMS as Platform[]).optional(),
48
53
  corpus: z
49
54
  .enum(['guide', 'recipe', 'design-note', 'implementation-note'])
50
55
  .optional()
@@ -2,4 +2,4 @@
2
2
  // layer); the MCP tool registration lives in ./tools.mjs. Both names are
3
3
  // re-exported here so existing importers keep working.
4
4
  export { createInspectionService } from '@octane-xplat/cli/mcp/inspection/service'
5
- export { registerInspectionTools } from './tools.mjs'
5
+ export { registerInspectionTools } from './tools.ts'