hammerkit 1.26.0 → 1.27.0-next.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.
- package/build.schema.json +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +41 -0
- package/dist/planner/work-runtime-kubernetes.js +335 -102
- package/dist/program.js +21 -0
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serveMcp = exports.createMcpTools = void 0;
|
|
4
|
+
const readline_1 = require("readline");
|
|
5
|
+
const format_1 = require("./format");
|
|
6
|
+
const sandbox_client_1 = require("./sandbox-client");
|
|
7
|
+
// the newest first; a client asking for an older one gets it
|
|
8
|
+
const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
9
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
10
|
+
const EDITS = {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: true,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
openWorldHint: false,
|
|
15
|
+
};
|
|
16
|
+
const text = (description) => ({ type: 'string', description });
|
|
17
|
+
const count = (description, minimum = 0, maximum) => ({
|
|
18
|
+
type: 'integer',
|
|
19
|
+
description,
|
|
20
|
+
minimum,
|
|
21
|
+
...(maximum !== undefined ? { maximum } : {}),
|
|
22
|
+
});
|
|
23
|
+
const flag = (description) => ({ type: 'boolean', description });
|
|
24
|
+
// MCP clients send paths as they like them: `/src/a.ts` is `src/a.ts` of the sandbox
|
|
25
|
+
function sandboxPath(path) {
|
|
26
|
+
return (path ?? '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
27
|
+
}
|
|
28
|
+
function readLog(run, task) {
|
|
29
|
+
return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
|
|
30
|
+
}
|
|
31
|
+
function describeRun(task, ran) {
|
|
32
|
+
const result = ran.result;
|
|
33
|
+
const lines = [`run ${ran.run.id}: ${ran.run.state}`];
|
|
34
|
+
if (result) {
|
|
35
|
+
lines.push(`task ${task}: ${result.status}` +
|
|
36
|
+
(result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
|
|
37
|
+
(result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
|
|
38
|
+
(result.cache?.hit ? ', from the cache' : ''));
|
|
39
|
+
if (result.error) {
|
|
40
|
+
lines.push(`error: ${result.error}`);
|
|
41
|
+
}
|
|
42
|
+
if (result.cache?.causes?.length) {
|
|
43
|
+
lines.push(`ran because: ${result.cache.causes.join('; ')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (ran.run.error) {
|
|
47
|
+
lines.push(`error: ${ran.run.error}`);
|
|
48
|
+
}
|
|
49
|
+
if (result && result.lastLog.length > 0) {
|
|
50
|
+
lines.push('last lines:', ...result.lastLog);
|
|
51
|
+
}
|
|
52
|
+
lines.push(readLog(ran.run.id, task));
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The tools of `hammerkit remote mcp`, for chat clients and agents without file and shell tools of
|
|
57
|
+
* their own. They take the shapes models are trained on: numbered reads with offset and
|
|
58
|
+
* limit, exact-string replace, `apply_patch` or a git diff, ripgrep's parameters, a shell
|
|
59
|
+
* command. Paths are relative to the root of the sandbox.
|
|
60
|
+
*/
|
|
61
|
+
function createMcpTools(client) {
|
|
62
|
+
return inOrder([
|
|
63
|
+
{
|
|
64
|
+
name: 'read',
|
|
65
|
+
description: 'Read a file of the sandbox. Lines come back numbered like `cat -n`: the number, a tab, the line. ' +
|
|
66
|
+
'Long files come in parts; offset is the first line (1-based), limit the number of lines.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
path: text('Path of the file, relative to the root of the sandbox'),
|
|
71
|
+
offset: count('The first line to read, 1 for the start', 1),
|
|
72
|
+
limit: count('How many lines to read', 1),
|
|
73
|
+
},
|
|
74
|
+
required: ['path'],
|
|
75
|
+
},
|
|
76
|
+
annotations: { title: 'Read a file', ...READ_ONLY },
|
|
77
|
+
async call(args) {
|
|
78
|
+
const read = await (await client()).read(sandboxPath(args.path), args.offset, args.limit);
|
|
79
|
+
const footer = read.nextOffset
|
|
80
|
+
? `(lines ${read.offset} to ${read.nextOffset - 1} of ${read.lines}, read on with offset ${read.nextOffset})`
|
|
81
|
+
: `(end of file, ${read.lines} lines)`;
|
|
82
|
+
return `${read.content.replace(/\n$/, '')}\n\n${footer}`;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'write',
|
|
87
|
+
description: 'Write a file of the sandbox, replacing what it held. Directories are made as needed.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: { path: text('Path of the file'), content: text('The whole new content of the file') },
|
|
91
|
+
required: ['path', 'content'],
|
|
92
|
+
},
|
|
93
|
+
annotations: { title: 'Write a file', ...EDITS, idempotentHint: true },
|
|
94
|
+
async call(args) {
|
|
95
|
+
const written = await (await client()).writeRaw(sandboxPath(args.path), args.content);
|
|
96
|
+
return `${written.change} ${written.path} (${written.size ?? Buffer.byteLength(args.content)} bytes)`;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'edit',
|
|
101
|
+
description: 'Replace an exact string in a file of the sandbox. old_string has to match exactly once, with its ' +
|
|
102
|
+
'whitespace and indentation, unless replace_all is set; give enough lines around the change to make it unique.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
path: text('Path of the file'),
|
|
107
|
+
old_string: text('The text to replace, exactly as it is in the file'),
|
|
108
|
+
new_string: text('The text to put in its place'),
|
|
109
|
+
replace_all: flag('Replace every occurrence of old_string'),
|
|
110
|
+
},
|
|
111
|
+
required: ['path', 'old_string', 'new_string'],
|
|
112
|
+
},
|
|
113
|
+
annotations: { title: 'Edit a file', ...EDITS },
|
|
114
|
+
async call(args) {
|
|
115
|
+
const files = await (await client()).edit([
|
|
116
|
+
{
|
|
117
|
+
op: 'replace',
|
|
118
|
+
path: sandboxPath(args.path),
|
|
119
|
+
old_string: args.old_string,
|
|
120
|
+
new_string: args.new_string,
|
|
121
|
+
replace_all: args.replace_all,
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
return files.map((file) => `${file.change} ${file.path}`).join('\n') || 'nothing changed';
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'apply_patch',
|
|
129
|
+
description: 'Change several files at once with a patch: the `*** Begin Patch` format (`*** Add File:`, ' +
|
|
130
|
+
'`*** Update File:` with `@@` context lines, `*** Delete File:`, `*** Move to:`) or a unified git diff. ' +
|
|
131
|
+
'Every file changes, or none does.',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: { patch: text('The patch, from `*** Begin Patch` to `*** End Patch`, or a git diff') },
|
|
135
|
+
required: ['patch'],
|
|
136
|
+
},
|
|
137
|
+
annotations: { title: 'Apply a patch', ...EDITS },
|
|
138
|
+
async call(args) {
|
|
139
|
+
const files = await (await client()).edit([{ op: 'patch', patch: args.patch }]);
|
|
140
|
+
return files.map((file) => `${file.change} ${file.path}`).join('\n') || 'nothing changed';
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'search',
|
|
145
|
+
description: 'Search the files of the sandbox with ripgrep. pattern is a regular expression (Rust syntax); output_mode ' +
|
|
146
|
+
'is files_with_matches (the default), content (matching lines, with -A/-B/-C context) or count.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
pattern: text('The regular expression to look for'),
|
|
151
|
+
path: text('A file or directory to search in, the whole sandbox by default'),
|
|
152
|
+
glob: text('Only files matching this glob, like "*.ts" or "src/**/*.tsx"'),
|
|
153
|
+
type: text('Only files of this ripgrep type, like ts, py or rust'),
|
|
154
|
+
'-i': flag('Ignore case'),
|
|
155
|
+
fixed_strings: flag('Take the pattern as a literal string'),
|
|
156
|
+
multiline: flag('Let the pattern match across lines'),
|
|
157
|
+
'-A': count('Lines after each match (content mode)', 0, 100),
|
|
158
|
+
'-B': count('Lines before each match (content mode)', 0, 100),
|
|
159
|
+
'-C': count('Lines before and after each match (content mode)', 0, 100),
|
|
160
|
+
output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'] },
|
|
161
|
+
head_limit: count('The most results to answer', 1, 5000),
|
|
162
|
+
},
|
|
163
|
+
required: ['pattern'],
|
|
164
|
+
},
|
|
165
|
+
annotations: { title: 'Search files', ...READ_ONLY },
|
|
166
|
+
async call(args) {
|
|
167
|
+
const query = { ...args, path: args.path ? sandboxPath(args.path) || undefined : undefined };
|
|
168
|
+
return (0, format_1.formatSearch)(await (await client()).search(query));
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'glob',
|
|
173
|
+
description: 'List the files of the sandbox that match a glob, like "**/*.ts" or "src/**/package.json".',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
pattern: text('The glob, relative to path'),
|
|
178
|
+
path: text('The directory to look in, the root by default'),
|
|
179
|
+
},
|
|
180
|
+
required: ['pattern'],
|
|
181
|
+
},
|
|
182
|
+
annotations: { title: 'Find files', ...READ_ONLY },
|
|
183
|
+
async call(args) {
|
|
184
|
+
const path = sandboxPath(args.path) || undefined;
|
|
185
|
+
return (0, format_1.formatTree)(await (await client()).tree({ path, glob: args.pattern, limit: 500 }));
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'bash',
|
|
190
|
+
description: 'Run a shell command on the sandbox and answer its output and exit code. It runs as the shell task of ' +
|
|
191
|
+
'the published build file, in its container and with its network; commands run one at a time, and ' +
|
|
192
|
+
'only the files are kept between them.',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
command: text('The command'),
|
|
197
|
+
workdir: text('The directory to run it in, relative to the root of the sandbox'),
|
|
198
|
+
timeout: count('Stop the command after this many seconds', 1),
|
|
199
|
+
},
|
|
200
|
+
required: ['command'],
|
|
201
|
+
},
|
|
202
|
+
annotations: { title: 'Run a command' },
|
|
203
|
+
async call(args, signal) {
|
|
204
|
+
const sandbox = await client();
|
|
205
|
+
const ran = await sandbox.exec(args.command, sandboxPath(args.workdir), {
|
|
206
|
+
signal,
|
|
207
|
+
timeoutSeconds: args.timeout,
|
|
208
|
+
});
|
|
209
|
+
const output = (0, format_1.capOutput)(ran.output, readLog(ran.run.id, sandbox.build.shell));
|
|
210
|
+
const lines = output ? [output, ''] : [];
|
|
211
|
+
if (ran.exitCode === null) {
|
|
212
|
+
lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
lines.push(`exit code ${ran.exitCode}`);
|
|
216
|
+
}
|
|
217
|
+
return lines.join('\n');
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'run_task',
|
|
222
|
+
description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
|
|
223
|
+
'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
|
|
224
|
+
'its last command, for example a test file.',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
task: text('The name of the task'),
|
|
229
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Arguments for its last command' },
|
|
230
|
+
},
|
|
231
|
+
required: ['task'],
|
|
232
|
+
},
|
|
233
|
+
annotations: { title: 'Run a task' },
|
|
234
|
+
async call(args, signal) {
|
|
235
|
+
const ran = await (await client()).runTask(args.task, { args: args.args, signal });
|
|
236
|
+
return describeRun(args.task, ran);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'run_logs',
|
|
241
|
+
description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
|
|
242
|
+
'the lines that contain a text (grep, a fixed string) with lines of context around them.',
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
run: text('The id of the run'),
|
|
247
|
+
task: text('Only the lines of this task'),
|
|
248
|
+
tail: count('The last lines, or with grep the last matches', 1, 1000),
|
|
249
|
+
from: count('The first line to read', 1),
|
|
250
|
+
limit: count('How many lines to read', 1, 1000),
|
|
251
|
+
grep: text('Only lines that contain this text'),
|
|
252
|
+
ignore_case: flag('Ignore case in grep'),
|
|
253
|
+
context: count('Lines before and after each match', 0, 20),
|
|
254
|
+
},
|
|
255
|
+
required: ['run'],
|
|
256
|
+
},
|
|
257
|
+
annotations: { title: 'Read the log of a run', ...READ_ONLY },
|
|
258
|
+
async call(args) {
|
|
259
|
+
const page = await (await client()).logs(args.run, {
|
|
260
|
+
task: args.task,
|
|
261
|
+
tail: args.tail,
|
|
262
|
+
from: args.from,
|
|
263
|
+
limit: args.limit,
|
|
264
|
+
grep: args.grep,
|
|
265
|
+
i: args.ignore_case,
|
|
266
|
+
context: args.context,
|
|
267
|
+
});
|
|
268
|
+
const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
|
|
269
|
+
lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'changes',
|
|
275
|
+
description: 'What changed in the sandbox since the commit it was fetched at: a summary of the files, a diff, or a ' +
|
|
276
|
+
'patch to apply elsewhere with git am.',
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
format: { type: 'string', enum: ['summary', 'diff', 'patch'], description: 'summary by default' },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
annotations: { title: 'Show the changes', ...READ_ONLY },
|
|
284
|
+
async call(args) {
|
|
285
|
+
const sandbox = await client();
|
|
286
|
+
if (args.format === 'diff' || args.format === 'patch') {
|
|
287
|
+
return (await sandbox.diff(args.format)) || 'no changes';
|
|
288
|
+
}
|
|
289
|
+
const changes = await sandbox.changes();
|
|
290
|
+
if (changes.files.length === 0) {
|
|
291
|
+
return `no changes since ${changes.commit}`;
|
|
292
|
+
}
|
|
293
|
+
return [
|
|
294
|
+
`changes since ${changes.commit}:`,
|
|
295
|
+
...changes.files.map((file) => `${file.status} ${file.from ? `${file.from} -> ` : ''}${file.path}` +
|
|
296
|
+
(file.additions !== null ? ` +${file.additions} -${file.deletions}` : ' (binary)')),
|
|
297
|
+
].join('\n');
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'push',
|
|
302
|
+
description: 'Commit the changes of the sandbox and push them to a branch of its repository, for review. The server ' +
|
|
303
|
+
'pushes only to the branches its operator allows; the sandbox is at the pushed commit afterwards.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
branch: text('the branch to push to, created when it does not exist'),
|
|
308
|
+
message: text('the commit message'),
|
|
309
|
+
},
|
|
310
|
+
required: ['branch', 'message'],
|
|
311
|
+
},
|
|
312
|
+
annotations: { title: 'Push a branch', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
313
|
+
async call(args) {
|
|
314
|
+
const pushed = await (await client()).push(args.branch, args.message);
|
|
315
|
+
return `pushed ${pushed.commit} to ${pushed.branch}`;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
exports.createMcpTools = createMcpTools;
|
|
321
|
+
// the calls of a client go to the sandbox one at a time, in the order the client sent them
|
|
322
|
+
function inOrder(tools) {
|
|
323
|
+
const inTurn = (0, sandbox_client_1.oneAtATime)();
|
|
324
|
+
return tools.map((tool) => ({ ...tool, call: (args, signal) => inTurn(() => tool.call(args, signal)) }));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A Model Context Protocol server over stdio: one JSON-RPC message per line on `input`, the
|
|
328
|
+
* answers on `output`. It answers `initialize`, `ping`, `tools/list` and `tools/call`, and
|
|
329
|
+
* stops a call when the client cancels it. Resolves when the input ends.
|
|
330
|
+
*/
|
|
331
|
+
async function serveMcp(input, output, tools, server) {
|
|
332
|
+
const running = new Map();
|
|
333
|
+
const pending = new Set();
|
|
334
|
+
const send = (message) => {
|
|
335
|
+
output.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
|
|
336
|
+
};
|
|
337
|
+
const reply = (id, result) => send({ id, result });
|
|
338
|
+
const fail = (id, code, message) => send({ id, error: { code, message } });
|
|
339
|
+
const handle = async (message) => {
|
|
340
|
+
const id = message.id;
|
|
341
|
+
if (message.method === 'notifications/cancelled') {
|
|
342
|
+
running.get(message.params?.requestId)?.abort();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (id === undefined || id === null || message.method === undefined) {
|
|
346
|
+
// a notification, or an answer of the client to a request this server never sends
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (message.method === 'initialize') {
|
|
350
|
+
const asked = message.params?.protocolVersion;
|
|
351
|
+
reply(id, {
|
|
352
|
+
protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
353
|
+
capabilities: { tools: { listChanged: false } },
|
|
354
|
+
serverInfo: { name: server.name, version: server.version },
|
|
355
|
+
...(server.instructions ? { instructions: server.instructions } : {}),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else if (message.method === 'ping') {
|
|
359
|
+
reply(id, {});
|
|
360
|
+
}
|
|
361
|
+
else if (message.method === 'tools/list') {
|
|
362
|
+
reply(id, {
|
|
363
|
+
tools: tools.map((tool) => ({
|
|
364
|
+
name: tool.name,
|
|
365
|
+
description: tool.description,
|
|
366
|
+
inputSchema: tool.inputSchema,
|
|
367
|
+
annotations: tool.annotations,
|
|
368
|
+
})),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else if (message.method === 'tools/call') {
|
|
372
|
+
const tool = tools.find((candidate) => candidate.name === message.params?.name);
|
|
373
|
+
if (!tool) {
|
|
374
|
+
return fail(id, -32602, `unknown tool ${String(message.params?.name)}`);
|
|
375
|
+
}
|
|
376
|
+
const args = message.params?.arguments ?? {};
|
|
377
|
+
const missing = (tool.inputSchema.required ?? []).filter((name) => args[name] === undefined);
|
|
378
|
+
if (missing.length > 0) {
|
|
379
|
+
return reply(id, { content: [{ type: 'text', text: `missing ${missing.join(', ')}` }], isError: true });
|
|
380
|
+
}
|
|
381
|
+
const abort = new AbortController();
|
|
382
|
+
running.set(id, abort);
|
|
383
|
+
try {
|
|
384
|
+
const text = await tool.call(args, abort.signal);
|
|
385
|
+
if (!abort.signal.aborted) {
|
|
386
|
+
reply(id, { content: [{ type: 'text', text }] });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
if (!abort.signal.aborted) {
|
|
391
|
+
// a failed call is an answer the model reads, not a protocol error
|
|
392
|
+
reply(id, { content: [{ type: 'text', text: e instanceof Error ? e.message : String(e) }], isError: true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
running.delete(id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
fail(id, -32601, `method not found: ${String(message.method)}`);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const lines = (0, readline_1.createInterface)({ input, crlfDelay: Infinity });
|
|
404
|
+
for await (const line of lines) {
|
|
405
|
+
if (line.trim().length === 0) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
let message;
|
|
409
|
+
try {
|
|
410
|
+
message = JSON.parse(line);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
fail(null, -32700, 'parse error');
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (typeof message !== 'object' || message === null || Array.isArray(message)) {
|
|
417
|
+
fail(null, -32600, 'invalid request');
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const work = handle(message).catch((e) => message.id !== undefined && message.id !== null
|
|
421
|
+
? fail(message.id, -32603, e instanceof Error ? e.message : String(e))
|
|
422
|
+
: undefined);
|
|
423
|
+
pending.add(work);
|
|
424
|
+
work.finally(() => pending.delete(work));
|
|
425
|
+
}
|
|
426
|
+
for (const abort of running.values()) {
|
|
427
|
+
abort.abort();
|
|
428
|
+
}
|
|
429
|
+
await Promise.all(pending);
|
|
430
|
+
}
|
|
431
|
+
exports.serveMcp = serveMcp;
|
|
432
|
+
//# sourceMappingURL=mcp.js.map
|