@brainervirus/workit-mcp 1.0.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/README.md +18 -0
- package/dist/index.js +27649 -0
- package/package.json +46 -0
- package/scripts/build.ts +33 -0
- package/src/index.ts +30 -0
- package/src/server.ts +388 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brainervirus/workit-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Workit shared MCP transport for Cursor and Codex",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent",
|
|
8
|
+
"codex",
|
|
9
|
+
"cursor",
|
|
10
|
+
"mcp",
|
|
11
|
+
"workflow"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bin": {
|
|
15
|
+
"workit-mcp": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/",
|
|
19
|
+
"src/",
|
|
20
|
+
"scripts/",
|
|
21
|
+
"package.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./dist/index.js",
|
|
29
|
+
"./src/*.ts": "./src/*.ts",
|
|
30
|
+
"./src/*": "./src/*.ts"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "bun scripts/build.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@brainervirus/workit-core": "^1.0.0",
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
41
|
+
"zod": "4.5.4"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const target = process.argv[2] ? path.resolve(process.argv[2]) : packageDir;
|
|
9
|
+
const dist = path.join(target, "dist");
|
|
10
|
+
rmSync(dist, { recursive: true, force: true });
|
|
11
|
+
mkdirSync(dist, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const build = spawnSync(
|
|
14
|
+
process.execPath,
|
|
15
|
+
[
|
|
16
|
+
"build",
|
|
17
|
+
path.join(packageDir, "src/index.ts"),
|
|
18
|
+
"--outfile",
|
|
19
|
+
path.join(dist, "index.js"),
|
|
20
|
+
"--target",
|
|
21
|
+
"node",
|
|
22
|
+
"--format",
|
|
23
|
+
"esm",
|
|
24
|
+
"--banner",
|
|
25
|
+
"#!/usr/bin/env node",
|
|
26
|
+
],
|
|
27
|
+
{ encoding: "utf8" },
|
|
28
|
+
);
|
|
29
|
+
if (build.status !== 0) {
|
|
30
|
+
process.stderr.write(build.stderr || build.stdout || "MCP build failed\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`mcp: built dist/index.js (${target})`);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertMcpHost, runStdioServer, sanitizeTransportText } from "./server";
|
|
4
|
+
import type { McpHost } from "./server";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
assertMcpHost,
|
|
8
|
+
createMcpServer,
|
|
9
|
+
McpCapabilityUnavailableError,
|
|
10
|
+
runStdioServer,
|
|
11
|
+
sanitizeTransportText,
|
|
12
|
+
type McpHost,
|
|
13
|
+
type NativeContextProvider,
|
|
14
|
+
} from "./server";
|
|
15
|
+
|
|
16
|
+
const main = async (): Promise<void> => {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const hostIndex = args.indexOf("--host");
|
|
19
|
+
const host = hostIndex >= 0 ? args[hostIndex + 1] : undefined;
|
|
20
|
+
try {
|
|
21
|
+
assertMcpHost(host);
|
|
22
|
+
await runStdioServer(host as McpHost);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
process.stderr.write(`${sanitizeTransportText(error)}\n`);
|
|
25
|
+
process.exitCode = 2;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const entryPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
30
|
+
if (entryPath === path.resolve(fileURLToPath(import.meta.url))) await main();
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListResourceTemplatesRequestSchema,
|
|
8
|
+
ListResourcesRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
ReadResourceRequestSchema,
|
|
11
|
+
type CallToolResult,
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
OPERATION_FAMILIES,
|
|
16
|
+
WorkitCore,
|
|
17
|
+
TaskStore,
|
|
18
|
+
boundedOperationJsonSchema,
|
|
19
|
+
parseOperation,
|
|
20
|
+
type OperationContext,
|
|
21
|
+
type OperationFamily,
|
|
22
|
+
} from "@brainervirus/workit-core/src/core";
|
|
23
|
+
import {
|
|
24
|
+
changedSourcesSinceLoad,
|
|
25
|
+
markSourcesLoaded,
|
|
26
|
+
} from "@brainervirus/workit-core/src/core/boundary";
|
|
27
|
+
import type { Host, Result } from "@brainervirus/workit-core/src/core/task-contract";
|
|
28
|
+
import { redactSecrets } from "@brainervirus/workit-core/src/core/logger";
|
|
29
|
+
import { readExternalContext } from "@brainervirus/workit-core/src/core/external-action-effects";
|
|
30
|
+
|
|
31
|
+
export type McpHost = Extract<Host, "cursor" | "codex_cli" | "codex_desktop">;
|
|
32
|
+
export type NativeContextProvider = { current(): Promise<OperationContext> };
|
|
33
|
+
|
|
34
|
+
export class McpCapabilityUnavailableError extends Error {
|
|
35
|
+
readonly capability: string;
|
|
36
|
+
|
|
37
|
+
constructor(capability: string) {
|
|
38
|
+
super(`${capability} unavailable`);
|
|
39
|
+
this.name = "McpCapabilityUnavailableError";
|
|
40
|
+
this.capability = capability;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class McpResourceInputError extends Error {}
|
|
45
|
+
|
|
46
|
+
const MCP_HOSTS = new Set<McpHost>(["cursor", "codex_cli", "codex_desktop"]);
|
|
47
|
+
const CONTEXT_KINDS = [
|
|
48
|
+
"git",
|
|
49
|
+
"pr",
|
|
50
|
+
"youtrack",
|
|
51
|
+
"github_issue",
|
|
52
|
+
"gitlab_issue",
|
|
53
|
+
"changelog",
|
|
54
|
+
"release",
|
|
55
|
+
"affected",
|
|
56
|
+
] as const;
|
|
57
|
+
type ContextKind = (typeof CONTEXT_KINDS)[number];
|
|
58
|
+
const CONTEXT_SELECTORS = ["range", "issueId"] as const;
|
|
59
|
+
const safeCapability = (value: string, allowed: readonly string[]): string =>
|
|
60
|
+
allowed.includes(value) ? value : "context";
|
|
61
|
+
const TOOL_CAPABILITIES = [
|
|
62
|
+
...CONTEXT_KINDS,
|
|
63
|
+
"workspace",
|
|
64
|
+
"native_caller_identity",
|
|
65
|
+
"external_action",
|
|
66
|
+
];
|
|
67
|
+
const VERSION = (() => {
|
|
68
|
+
try {
|
|
69
|
+
const packageJson = JSON.parse(
|
|
70
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
71
|
+
) as { version?: unknown };
|
|
72
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
73
|
+
} catch {
|
|
74
|
+
return "0.0.0";
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
const rootVariants = (workspaceRoot?: string): string[] => {
|
|
79
|
+
if (!workspaceRoot) return [];
|
|
80
|
+
const roots = new Set<string>();
|
|
81
|
+
for (const candidate of [workspaceRoot, path.resolve(workspaceRoot)]) {
|
|
82
|
+
if (path.isAbsolute(candidate) && candidate !== path.parse(candidate).root)
|
|
83
|
+
roots.add(candidate);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const canonical = realpathSync(workspaceRoot);
|
|
87
|
+
if (canonical !== path.parse(canonical).root) roots.add(canonical);
|
|
88
|
+
} catch {
|
|
89
|
+
// The core will return a structured failure for an unavailable root.
|
|
90
|
+
}
|
|
91
|
+
return [...roots].sort((left, right) => right.length - left.length);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const sanitizeTransportText = (value: unknown, workspaceRoot?: string): string => {
|
|
95
|
+
const message = value instanceof Error ? value.message : String(value);
|
|
96
|
+
const withoutStack = message.split(/\r?\n/, 1)[0].replace(/\s+(?:at|stack:)\s.*$/i, "");
|
|
97
|
+
const withoutRoot = rootVariants(workspaceRoot).reduce(
|
|
98
|
+
(current, root) => current.split(root).join("[WORKSPACE_ROOT]"),
|
|
99
|
+
withoutStack,
|
|
100
|
+
);
|
|
101
|
+
return redactSecrets(withoutRoot).slice(0, 500);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const reportError = (tool: string, error: unknown, workspaceRoot?: string): void => {
|
|
105
|
+
try {
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`${JSON.stringify({ level: "error", message: "MCP tool failed", tool, error: sanitizeTransportText(error, workspaceRoot) })}\n`,
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
// Diagnostics must never break the protocol response.
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function assertMcpHost(host: unknown): asserts host is McpHost {
|
|
115
|
+
if (typeof host !== "string" || !MCP_HOSTS.has(host as McpHost)) {
|
|
116
|
+
throw new Error("MCP host must be cursor, codex_cli, or codex_desktop");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const operationDescription = (family: OperationFamily): string =>
|
|
121
|
+
`Workit ${family} operations. Inputs are validated by the shared Workit contract.`;
|
|
122
|
+
|
|
123
|
+
const READ_ONLY_ACTIONS = new Set(["list", "inspect", "preview", "explain", "export"]);
|
|
124
|
+
const requiresCallerIdentity = (input: unknown): boolean =>
|
|
125
|
+
typeof input !== "object" ||
|
|
126
|
+
input === null ||
|
|
127
|
+
!READ_ONLY_ACTIONS.has(String((input as { action?: unknown }).action));
|
|
128
|
+
|
|
129
|
+
const toolInputSchema = (family: OperationFamily) => {
|
|
130
|
+
const schema = boundedOperationJsonSchema(family);
|
|
131
|
+
// MCP requires an object at the root. The operation union remains entirely
|
|
132
|
+
// core-derived; this envelope preserves it while satisfying that protocol rule.
|
|
133
|
+
return { type: "object" as const, ...schema };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const sanitizeFailure = (result: Result<unknown>, workspaceRoot?: string): Result<unknown> => {
|
|
137
|
+
if (result.ok) return result;
|
|
138
|
+
const sanitize = (value: unknown): unknown => {
|
|
139
|
+
if (typeof value === "string") return sanitizeTransportText(value, workspaceRoot);
|
|
140
|
+
if (Array.isArray(value)) return value.map(sanitize);
|
|
141
|
+
if (value !== null && typeof value === "object") {
|
|
142
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)]));
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
};
|
|
146
|
+
return sanitize(result) as Result<unknown>;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const resultForClient = (result: Result<unknown>, workspaceRoot?: string): CallToolResult => {
|
|
150
|
+
const safe = sanitizeFailure(result, workspaceRoot);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: JSON.stringify(safe) }],
|
|
153
|
+
structuredContent: safe,
|
|
154
|
+
...(safe.ok ? {} : { isError: true }),
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const thrownResult = (tool: string, error: unknown, workspaceRoot?: string): CallToolResult => {
|
|
159
|
+
reportError(tool, error, workspaceRoot);
|
|
160
|
+
if (error instanceof McpCapabilityUnavailableError) {
|
|
161
|
+
const capability = safeCapability(error.capability, TOOL_CAPABILITIES);
|
|
162
|
+
return resultForClient(
|
|
163
|
+
{
|
|
164
|
+
ok: false,
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
code: "capability_unavailable",
|
|
167
|
+
error: `${capability} unavailable`,
|
|
168
|
+
details: { capability },
|
|
169
|
+
},
|
|
170
|
+
workspaceRoot,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const result = {
|
|
174
|
+
ok: false as const,
|
|
175
|
+
schemaVersion: 1 as const,
|
|
176
|
+
code: "storage_error" as const,
|
|
177
|
+
error: "MCP operation failed",
|
|
178
|
+
details: {},
|
|
179
|
+
};
|
|
180
|
+
return resultForClient(result, workspaceRoot);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function createMcpServer(host: McpHost, contextProvider: NativeContextProvider): Server {
|
|
184
|
+
assertMcpHost(host);
|
|
185
|
+
// Long-lived MCP servers load core once; warn once (never block) when the
|
|
186
|
+
// checkout sources move underneath, mirroring the OpenCode plugin guard.
|
|
187
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
188
|
+
const sourceMarker = markSourcesLoaded(
|
|
189
|
+
[
|
|
190
|
+
path.join(here, "server.ts"),
|
|
191
|
+
path.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
|
|
192
|
+
path.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts"),
|
|
193
|
+
].filter((file) => existsSync(file)),
|
|
194
|
+
);
|
|
195
|
+
let staleWarned = false;
|
|
196
|
+
const server = new Server(
|
|
197
|
+
{ name: "workit", version: VERSION },
|
|
198
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
202
|
+
tools: OPERATION_FAMILIES.map((family) => ({
|
|
203
|
+
name: `workit_${family}`,
|
|
204
|
+
description: operationDescription(family),
|
|
205
|
+
inputSchema: toolInputSchema(family),
|
|
206
|
+
})),
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
210
|
+
resources: CONTEXT_KINDS.map((kind) => ({
|
|
211
|
+
uri: `workit://context/${kind}`,
|
|
212
|
+
name: `Workit ${kind} context`,
|
|
213
|
+
description: "Read-only context derived from the host-owned workspace.",
|
|
214
|
+
mimeType: "application/json",
|
|
215
|
+
})),
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
219
|
+
resourceTemplates: [
|
|
220
|
+
{
|
|
221
|
+
uriTemplate: "workit://context/{kind}{?range,issueId}",
|
|
222
|
+
name: "Workit context",
|
|
223
|
+
description: "Read-only context; workspace and caller come from the host session.",
|
|
224
|
+
mimeType: "application/json",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
230
|
+
let workspaceRoot: string | undefined;
|
|
231
|
+
try {
|
|
232
|
+
const context = await contextProvider.current();
|
|
233
|
+
workspaceRoot = context.root;
|
|
234
|
+
if (context.caller.host !== host)
|
|
235
|
+
throw new Error("native context host does not match MCP host");
|
|
236
|
+
const parsed = new URL(request.params.uri);
|
|
237
|
+
if (parsed.protocol !== "workit:" || parsed.hostname !== "context")
|
|
238
|
+
throw new McpResourceInputError("unsupported Workit context URI");
|
|
239
|
+
const kind = parsed.pathname.replace(/^\//, "") as ContextKind;
|
|
240
|
+
if (!CONTEXT_KINDS.includes(kind))
|
|
241
|
+
throw new McpResourceInputError("unsupported Workit context kind");
|
|
242
|
+
for (const key of parsed.searchParams.keys())
|
|
243
|
+
if (!CONTEXT_SELECTORS.includes(key as (typeof CONTEXT_SELECTORS)[number]))
|
|
244
|
+
throw new McpResourceInputError("unsupported Workit context selector");
|
|
245
|
+
const result = await readExternalContext(context.root, {
|
|
246
|
+
kind,
|
|
247
|
+
...(parsed.searchParams.get("range") ? { range: parsed.searchParams.get("range")! } : {}),
|
|
248
|
+
...(parsed.searchParams.get("issueId")
|
|
249
|
+
? { issueId: parsed.searchParams.get("issueId")! }
|
|
250
|
+
: {}),
|
|
251
|
+
});
|
|
252
|
+
if (!result.ok) throw new McpCapabilityUnavailableError(result.details.capability ?? kind);
|
|
253
|
+
return {
|
|
254
|
+
contents: [
|
|
255
|
+
{
|
|
256
|
+
uri: request.params.uri,
|
|
257
|
+
mimeType: "application/json",
|
|
258
|
+
text: JSON.stringify(result.data),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
reportError("context.read", error, workspaceRoot);
|
|
264
|
+
const capability =
|
|
265
|
+
error instanceof McpCapabilityUnavailableError
|
|
266
|
+
? safeCapability(error.capability, CONTEXT_KINDS)
|
|
267
|
+
: "context";
|
|
268
|
+
const safe =
|
|
269
|
+
error instanceof McpCapabilityUnavailableError
|
|
270
|
+
? {
|
|
271
|
+
ok: false,
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
code: "capability_unavailable",
|
|
274
|
+
error: `${capability} unavailable`,
|
|
275
|
+
details: { capability },
|
|
276
|
+
}
|
|
277
|
+
: error instanceof McpResourceInputError
|
|
278
|
+
? {
|
|
279
|
+
ok: false,
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
code: "invalid_input",
|
|
282
|
+
error: "invalid context resource request",
|
|
283
|
+
details: {},
|
|
284
|
+
}
|
|
285
|
+
: {
|
|
286
|
+
ok: false,
|
|
287
|
+
schemaVersion: 1,
|
|
288
|
+
code: "storage_error",
|
|
289
|
+
error: "MCP operation failed",
|
|
290
|
+
details: {},
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
contents: [
|
|
294
|
+
{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(safe) },
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
301
|
+
if (!staleWarned && changedSourcesSinceLoad(sourceMarker).length > 0) {
|
|
302
|
+
staleWarned = true;
|
|
303
|
+
try {
|
|
304
|
+
process.stderr.write(
|
|
305
|
+
`${JSON.stringify({ level: "warn", message: "workit sources changed after MCP load; restart the server for latest behavior" })}\n`,
|
|
306
|
+
);
|
|
307
|
+
} catch {
|
|
308
|
+
// Diagnostics must never break the protocol response.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const toolName = request.params.name;
|
|
312
|
+
let workspaceRoot: string | undefined;
|
|
313
|
+
try {
|
|
314
|
+
const family = OPERATION_FAMILIES.find((candidate) => toolName === `workit_${candidate}`);
|
|
315
|
+
if (!family) {
|
|
316
|
+
return resultForClient({
|
|
317
|
+
ok: false,
|
|
318
|
+
schemaVersion: 1,
|
|
319
|
+
code: "invalid_input",
|
|
320
|
+
error: sanitizeTransportText(`Unknown Workit tool: ${toolName}`),
|
|
321
|
+
details: { fields: [{ path: "name", reason: "unknown operation family" }] },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const context = await contextProvider.current();
|
|
326
|
+
workspaceRoot = context.root;
|
|
327
|
+
if (context.caller.host !== host) {
|
|
328
|
+
return resultForClient(
|
|
329
|
+
{
|
|
330
|
+
ok: false,
|
|
331
|
+
schemaVersion: 1,
|
|
332
|
+
code: "invalid_input",
|
|
333
|
+
error: "native context host does not match MCP host",
|
|
334
|
+
details: { operation: family },
|
|
335
|
+
},
|
|
336
|
+
workspaceRoot,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const parsed = parseOperation(family, request.params.arguments);
|
|
340
|
+
if (!parsed.ok) return resultForClient(parsed, workspaceRoot);
|
|
341
|
+
if (context.callerAttested === false && requiresCallerIdentity(parsed.data))
|
|
342
|
+
return resultForClient(
|
|
343
|
+
{
|
|
344
|
+
ok: false,
|
|
345
|
+
schemaVersion: 1,
|
|
346
|
+
code: "capability_unavailable",
|
|
347
|
+
error:
|
|
348
|
+
"native caller identity is unavailable; run the workit CLI for mutations: node_modules/.bin/workit <family> <action> --json --confirm (bind a writer with --actor <session-id>)",
|
|
349
|
+
details: { capability: "native_caller_identity", operation: family },
|
|
350
|
+
},
|
|
351
|
+
workspaceRoot,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const core = new WorkitCore(new TaskStore(context.root), context);
|
|
355
|
+
const run = core[family] as unknown as (input: unknown) => Result<unknown>;
|
|
356
|
+
return resultForClient(run.call(core, parsed.data), workspaceRoot);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return thrownResult(toolName, error, workspaceRoot);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return server;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const defaultContextProvider = (host: McpHost): NativeContextProvider => {
|
|
366
|
+
const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
367
|
+
const actor = process.env.WORKFLOW_SESSION_ID ?? "";
|
|
368
|
+
return {
|
|
369
|
+
current: async () => ({
|
|
370
|
+
root,
|
|
371
|
+
caller: { host, actor },
|
|
372
|
+
callerAttested: actor !== "",
|
|
373
|
+
capabilities: [],
|
|
374
|
+
constraints: [],
|
|
375
|
+
now: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
376
|
+
}),
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export async function runStdioServer(
|
|
381
|
+
host: McpHost,
|
|
382
|
+
contextProvider: NativeContextProvider = defaultContextProvider(host),
|
|
383
|
+
): Promise<Server> {
|
|
384
|
+
assertMcpHost(host);
|
|
385
|
+
const server = createMcpServer(host, contextProvider);
|
|
386
|
+
await server.connect(new StdioServerTransport());
|
|
387
|
+
return server;
|
|
388
|
+
}
|