@octane-xplat/mcp 0.1.0 → 0.16.0
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/LICENSE +21 -0
- package/README.md +342 -1
- package/dist/_virtual/_rolldown/runtime.js +27 -0
- package/dist/actions/bridge.js +1 -0
- package/dist/context.js +0 -0
- package/dist/contracts/actions.js +1 -0
- package/dist/contracts/boundaries.js +1 -0
- package/dist/contracts/capabilities.js +1 -0
- package/dist/contracts/code.js +1 -0
- package/dist/contracts/errors.js +8 -0
- package/dist/contracts/evidence.js +8 -0
- package/dist/contracts/identity.js +8 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/inspection.js +1 -0
- package/dist/contracts/policy.js +8 -0
- package/dist/contracts/sessions.js +1 -0
- package/dist/contracts/status.js +1 -0
- package/dist/discovery.js +8 -0
- package/dist/docs.js +63 -0
- package/dist/inspection/service.js +3 -0
- package/dist/inspection/tools.js +43 -0
- package/dist/runtime/logs.js +1 -0
- package/dist/runtime/process.js +1 -0
- package/dist/runtime/service.js +1 -0
- package/dist/runtime/targets.js +1 -0
- package/dist/runtime/tools.js +119 -0
- package/dist/server.js +324 -0
- package/dist/status.js +8 -0
- package/dist/xplat-mcp.js +68 -0
- package/package.json +43 -4
- package/src/actions/bridge.ts +2 -0
- package/src/context.ts +108 -0
- package/src/contracts/actions.ts +3 -0
- package/src/contracts/boundaries.ts +3 -0
- package/src/contracts/capabilities.ts +3 -0
- package/src/contracts/code.ts +3 -0
- package/src/contracts/errors.ts +3 -0
- package/src/contracts/evidence.ts +3 -0
- package/src/contracts/identity.ts +3 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/inspection.ts +3 -0
- package/src/contracts/policy.ts +3 -0
- package/src/contracts/sessions.ts +3 -0
- package/src/contracts/status.ts +3 -0
- package/src/discovery.ts +2 -0
- package/src/docs.ts +89 -0
- package/src/inspection/service.ts +5 -0
- package/src/inspection/tools.ts +61 -0
- package/src/runtime/logs.ts +2 -0
- package/src/runtime/process.ts +2 -0
- package/src/runtime/service.ts +2 -0
- package/src/runtime/targets.ts +2 -0
- package/src/runtime/tools.ts +136 -0
- package/src/server.ts +580 -0
- package/src/status.ts +2 -0
- package/src/xplat-mcp.ts +96 -0
- package/types/contracts/actions.d.ts +209 -0
- package/types/contracts/boundaries.d.ts +6 -0
- package/types/contracts/capabilities.d.ts +106 -0
- package/types/contracts/code.d.ts +134 -0
- package/types/contracts/errors.d.ts +75 -0
- package/types/contracts/evidence.d.ts +115 -0
- package/types/contracts/identity.d.ts +127 -0
- package/types/contracts/index.d.ts +11 -0
- package/types/contracts/inspection.d.ts +45 -0
- package/types/contracts/policy.d.ts +86 -0
- package/types/contracts/sessions.d.ts +160 -0
- package/types/contracts/status.d.ts +40 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { identity_exports } from "../contracts/identity.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/runtime/tools.ts
|
|
4
|
+
function registerRuntimeTools(server, context, respond) {
|
|
5
|
+
const runtime = context.runtime;
|
|
6
|
+
server.registerTool("xplat_targets", {
|
|
7
|
+
description: "Discover launchable targets for the bound app: simulators, emulators, devices and host targets with availability, missing-toolchain reasons, and session busy/ownership state.",
|
|
8
|
+
inputSchema: { platform: z.enum(identity_exports.PLATFORMS).optional().describe("Limit to one platform") }
|
|
9
|
+
}, async ({ platform }) => {
|
|
10
|
+
const result = await runtime.listTargets({ platform });
|
|
11
|
+
return result.error ? respond({
|
|
12
|
+
ok: false,
|
|
13
|
+
error: result.error
|
|
14
|
+
}) : respond({
|
|
15
|
+
ok: true,
|
|
16
|
+
data: result
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
server.registerTool("xplat_build", {
|
|
20
|
+
description: "Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_logs. Repeating with the same requestKey returns the live job instead of duplicating it.",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
platform: z.enum(identity_exports.PLATFORMS),
|
|
23
|
+
requestKey: z.string().optional().describe("Idempotence key for retries")
|
|
24
|
+
}
|
|
25
|
+
}, async ({ platform, requestKey }) => {
|
|
26
|
+
const result = await runtime.build({
|
|
27
|
+
platform,
|
|
28
|
+
requestKey
|
|
29
|
+
});
|
|
30
|
+
return result.error ? respond({
|
|
31
|
+
ok: false,
|
|
32
|
+
error: result.error
|
|
33
|
+
}) : respond({
|
|
34
|
+
ok: true,
|
|
35
|
+
data: result
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
server.registerTool("xplat_launch", {
|
|
39
|
+
description: "Start an owned development session: bind one app to one target and run the platform dev command as a job. The job completes when readiness is confirmed — a failed launch after a successful build reports accurately, and the session keeps the app alive until xplat_session_end.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
platform: z.enum(identity_exports.PLATFORMS),
|
|
42
|
+
targetId: z.string().optional().describe("Target from xplat_targets; required when several are available"),
|
|
43
|
+
requestKey: z.string().optional().describe("Idempotence key for retries"),
|
|
44
|
+
browserMode: z.enum(["headless", "headed"]).optional().describe("Web only. Headed mode enables scoped OS keyboard input on qualified hosts; reserve desktop input before using it.")
|
|
45
|
+
}
|
|
46
|
+
}, async ({ platform, targetId, requestKey, browserMode }) => {
|
|
47
|
+
const result = await runtime.launch({
|
|
48
|
+
platform,
|
|
49
|
+
targetId,
|
|
50
|
+
requestKey,
|
|
51
|
+
browserMode
|
|
52
|
+
});
|
|
53
|
+
return result.error ? respond({
|
|
54
|
+
ok: false,
|
|
55
|
+
error: result.error
|
|
56
|
+
}) : respond({
|
|
57
|
+
ok: true,
|
|
58
|
+
data: result
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
server.registerTool("xplat_attach", {
|
|
62
|
+
description: "Attach to a compatible development session that is already running — never restarts or terminates it. Reports candidates per platform; attach binds a preserved user-owned session.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
platform: z.enum(identity_exports.PLATFORMS),
|
|
65
|
+
targetId: z.string().optional().describe("Candidate target; required when several are attachable")
|
|
66
|
+
}
|
|
67
|
+
}, async ({ platform, targetId }) => {
|
|
68
|
+
const result = await runtime.attach({
|
|
69
|
+
platform,
|
|
70
|
+
targetId
|
|
71
|
+
});
|
|
72
|
+
return result.error ? respond({
|
|
73
|
+
ok: false,
|
|
74
|
+
error: result.error
|
|
75
|
+
}) : respond({
|
|
76
|
+
ok: true,
|
|
77
|
+
data: result
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
server.registerTool("xplat_logs", {
|
|
81
|
+
description: "Bounded, ordered log read for a session or build job. Cursor-paginated: pass the previous nextCursor to continue; truncated/dropped flags reveal gaps and reset marks a restarted capture. A stopped or disconnected session reports its capture as historical.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
sessionId: z.string().optional(),
|
|
84
|
+
jobId: z.string().optional(),
|
|
85
|
+
cursor: z.number().optional().describe("Continue after this record seq"),
|
|
86
|
+
limit: z.number().int().positive().max(5e3).optional(),
|
|
87
|
+
stream: z.enum([
|
|
88
|
+
"stdout",
|
|
89
|
+
"stderr",
|
|
90
|
+
"lifecycle"
|
|
91
|
+
]).optional()
|
|
92
|
+
}
|
|
93
|
+
}, async ({ sessionId, jobId, cursor, limit, stream }) => {
|
|
94
|
+
if (!sessionId && !jobId) return respond({
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {
|
|
97
|
+
code: "invalid_selection",
|
|
98
|
+
message: "Pass a sessionId or a jobId.",
|
|
99
|
+
recovery: { action: "Use xplat_sessions or a build/launch job id." }
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const result = runtime.readLogs({
|
|
103
|
+
sessionId,
|
|
104
|
+
jobId,
|
|
105
|
+
cursor,
|
|
106
|
+
limit,
|
|
107
|
+
stream
|
|
108
|
+
});
|
|
109
|
+
return result.error ? respond({
|
|
110
|
+
ok: false,
|
|
111
|
+
error: result.error
|
|
112
|
+
}) : respond({
|
|
113
|
+
ok: true,
|
|
114
|
+
data: result
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { registerRuntimeTools };
|
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,14 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "MCP server for Octane Xplat consumer apps — project identity, capability discovery, permission and evidence contracts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
9
9
|
"directory": "packages/mcp"
|
|
10
10
|
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"xplat-mcp": "./dist/xplat-mcp.js"
|
|
13
|
+
},
|
|
11
14
|
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"types",
|
|
17
|
+
"dist",
|
|
12
18
|
"README.md"
|
|
13
|
-
]
|
|
14
|
-
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./contracts": {
|
|
23
|
+
"types": "./types/contracts/index.d.ts",
|
|
24
|
+
"default": "./dist/contracts/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./contracts/*": {
|
|
27
|
+
"types": "./types/contracts/*.d.ts",
|
|
28
|
+
"default": "./dist/contracts/*.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
37
|
+
"@octane-xplat/cli": "0.16.0",
|
|
38
|
+
"yaml": "2.9.1",
|
|
39
|
+
"zod": "4.6.5"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "24.10.1",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"vite": "8.3.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
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",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
52
|
+
}
|
|
53
|
+
}
|