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