@shardflux/mcp 0.5.1 → 0.6.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/CHANGELOG.md +57 -18
- package/README.md +52 -51
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +30 -19
- package/dist/http.d.ts +1 -12
- package/dist/http.js +4 -20
- package/dist/server.d.ts +70 -4
- package/dist/server.js +114 -31
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Every tool, field and variable the README shows is available from the version named here.
|
|
4
|
-
|
|
3
|
+
Every tool, field and variable the README shows is available from the version named here. Breaking changes ship in
|
|
4
|
+
minor releases and are marked **Breaking**. Versions before 0.3.0 were not published.
|
|
5
|
+
|
|
6
|
+
## 0.5.2
|
|
7
|
+
|
|
8
|
+
Needs `@shardflux/sdk` 0.11.1 (the workspace version).
|
|
9
|
+
|
|
10
|
+
Wording: the server instructions, tool descriptions and error hints say what to do, without internals (no behaviour
|
|
11
|
+
change).
|
|
12
|
+
|
|
13
|
+
- `send_feedback` goes to the Shardflux team (`note`: `Delivered to the Shardflux team. Keep sending feedback as you
|
|
14
|
+
work.`); the instructions and description ask agents to also pass on what their user asks for (a capability, an
|
|
15
|
+
option or a smoother workflow) and their ideas. `error_code` shows `e.g. template_not_found`.
|
|
16
|
+
- The instructions: a queued start (open, resume, fork) has a deadline 15 minutes after it was created; past it, it
|
|
17
|
+
fails with code `operation_failed`, `details.error_code` `capacity_unavailable` and `retryable` true. Nothing was
|
|
18
|
+
started, so send it again. The `capacity_unavailable` message ends `The start passed its deadline and nothing was
|
|
19
|
+
started; send it again.`
|
|
20
|
+
- `exec` on a file-first workspace and the instructions say `an execution runs to completion` (before: "cannot be
|
|
21
|
+
canceled"); the timeout message says `(a file-first execution runs to completion)`.
|
|
22
|
+
- Hints: `no_execution_host`: `The execution cannot be placed right now and nothing ran; retry in a few seconds.`
|
|
23
|
+
`host_feature_unavailable`: `search_files is not available for this workspace. Search with exec instead, ...
|
|
24
|
+
Retrying does not help.` (the same for `edit_file` and other tools). `mode_not_available` drops "yet".
|
|
25
|
+
|
|
26
|
+
## 0.6.0 (release candidate)
|
|
27
|
+
|
|
28
|
+
Completed exec output streams are drained before releasing their HTTP connections, with a bounded cleanup if a peer does not close.
|
|
29
|
+
|
|
30
|
+
Uses SDK 0.12.0 and its pooled transport. Adds label creation/filtering, workspace_idle, workspace_keepalive, workspace_set_idle_policy, workspace_set_labels, workspace_exec_start and workspace_exec_input. Exec tools follow key permissions and are hidden for file-first workspaces. Protocol errors include source.
|
|
31
|
+
|
|
32
|
+
Requires the QM integration backend release for labels, failed recovery, key reuse and pipe stdin. No production deployment has occurred from this branch.
|
|
33
|
+
|
|
34
|
+
### Instant suspend: durable storage
|
|
35
|
+
|
|
36
|
+
- Operations in tool results carry `durable`, `suspend_path`, `durability` (suspend, fork) and `lost_suspend` (resume)
|
|
37
|
+
when the result has them; `timing.server` adds `durable` and `durability_state`.
|
|
38
|
+
- `workspace_suspend` takes `durable: true`: returns once the copy is in durable storage (implies `wait`; not with
|
|
39
|
+
`after_seconds`). A copy that cannot be made is the tool error `durability_lost` (`operation_id`, `details.state`
|
|
40
|
+
`lost`, `details.reason`); a durable wait that runs out is `timeout` with `details` `{durable: false, state:
|
|
41
|
+
"pending"}` and says to call `operation_wait` with `durable: true`.
|
|
42
|
+
- `operation_wait` takes `durable: true` (a suspend or fork).
|
|
43
|
+
- A resume whose result carries `lost_suspend` adds a `notice` naming the checkpoint it resumed from.
|
|
44
|
+
- A suspend-when-idle canceled `workspace_active`: the `operation_failed` message says the workspace keeps running.
|
|
45
|
+
- `workspace_suspend` no longer describes the suspend as a "durable" checkpoint.
|
|
5
46
|
|
|
6
47
|
## 0.5.1
|
|
7
48
|
|
|
@@ -16,8 +57,7 @@ it. The result's `message` then says `the workspace is suspended as soon as it i
|
|
|
16
57
|
|
|
17
58
|
Needs `@shardflux/sdk` 0.11.0 (the workspace version).
|
|
18
59
|
|
|
19
|
-
A resume that restarted processes says so (cold boot,
|
|
20
|
-
suspend and no host can restore the memory snapshot, the cell resumes the workspace by booting its saved disk,
|
|
60
|
+
A resume that restarted processes says so (cold boot). After a platform runtime change, the cell resumes the workspace by booting its saved disk,
|
|
21
61
|
automatically, also when a workspace tool wakes it. Files are as of the suspend; every process was restarted.
|
|
22
62
|
|
|
23
63
|
- `timing.server` of results and error results adds `memory_restored` (when the API reports it: `false` for a cold
|
|
@@ -59,7 +99,7 @@ Opt-in overage with a spend cap (through `@shardflux/sdk` 0.10.0):
|
|
|
59
99
|
- A start refused with 402 `allowance_exhausted` comes back with `reason` `allowance_used`, `overage_paused` or
|
|
60
100
|
`spend_cap_reached` and `details.spend_cap` (errors already carried `details`).
|
|
61
101
|
|
|
62
|
-
Suspend when idle
|
|
102
|
+
Suspend when idle:
|
|
63
103
|
|
|
64
104
|
- `workspace_suspend` takes an optional `after_seconds` (30-3600): suspend when idle instead of now. The workspace is
|
|
65
105
|
suspended once it has been idle that long; the agent's next tool call on it cancels that, and a running command or a
|
|
@@ -69,11 +109,11 @@ Suspend when idle (contracts §20.6):
|
|
|
69
109
|
- Workspace summaries (`workspace_status`, `workspace_list`, `workspace_open`) carry `suspend_request`: the pending
|
|
70
110
|
request, or null.
|
|
71
111
|
|
|
72
|
-
## 0.4.0
|
|
112
|
+
## 0.4.0
|
|
73
113
|
|
|
74
114
|
Needs `@shardflux/sdk` 0.9.0 (the workspace version).
|
|
75
115
|
|
|
76
|
-
File-first workspaces
|
|
116
|
+
File-first workspaces:
|
|
77
117
|
|
|
78
118
|
- `workspace_open` takes `mode` (`processful` | `file_first`); `SHARDFLUX_WORKSPACE_MODE` sets the default. Workspace
|
|
79
119
|
results (`workspace_open`, `workspace_status`, `workspace_list`) carry `mode` and, for file-first workspaces,
|
|
@@ -92,7 +132,7 @@ File-first workspaces (contracts §29):
|
|
|
92
132
|
`current_tree_revision`), `outside_tree_root`, `execution_in_progress`, `execution_id_reused`, `no_execution_host`.
|
|
93
133
|
- The server instructions describe file-first workspaces.
|
|
94
134
|
|
|
95
|
-
Waking a suspended workspace is one request (
|
|
135
|
+
Waking a suspended workspace is one request (through `@shardflux/sdk` 0.9.0):
|
|
96
136
|
|
|
97
137
|
- A workspace tool on a suspended workspace wakes it with one held resume that returns the running workspace and this
|
|
98
138
|
server's tool token (its agent label), then runs the tool; the wake's `timing` is one `request(held)` phase. An API
|
|
@@ -100,7 +140,7 @@ Waking a suspended workspace is one request (contracts §22.6, through `@shardfl
|
|
|
100
140
|
- `workspace_resume` with `wait` goes through the server's workspace handle: one held request, after which the handle
|
|
101
141
|
holds the view and the token, so the next workspace tool starts without a token request.
|
|
102
142
|
|
|
103
|
-
File tools and the wake hint
|
|
143
|
+
File tools and the wake hint:
|
|
104
144
|
|
|
105
145
|
- `search_files` (read-only) and `edit_file` come from the SDK's `workspaceTools()` with its schemas, plus
|
|
106
146
|
`workspace_key` and `timeout_ms`, filtered by the `files` permission. The server instructions describe them.
|
|
@@ -108,19 +148,19 @@ File tools and the wake hint (contracts §26):
|
|
|
108
148
|
fire-and-forget; a suspended workspace starts resuming in the background, bounded like any wake by
|
|
109
149
|
`SHARDFLUX_WAKE_TIMEOUT_MS` and the call's deadline), except `read_file`, `list_files` and `search_files`.
|
|
110
150
|
- `read_file`, `list_files` and `search_files` of a suspended workspace whose disk a host still holds are answered
|
|
111
|
-
from that disk without resuming it
|
|
151
|
+
from that disk without resuming it, also as the first call after the suspend: the API now issues
|
|
112
152
|
tool tokens for suspended workspaces. Every other tool still resumes it. With an older API they resume it as before.
|
|
113
|
-
- A workspace
|
|
153
|
+
- A workspace without search and patches:
|
|
114
154
|
`search_files` and `edit_file` return `conflict`, `reason: host_feature_unavailable` (`details.feature`
|
|
115
155
|
`file_search` or `file_patch`), `retryable: false`, with a `hint` naming another way (`exec` with `grep`;
|
|
116
|
-
`read_file` then `write_file`).
|
|
156
|
+
`read_file` then `write_file`). `hostFeatureHint()` is
|
|
117
157
|
exported next to `modeHint()`.
|
|
118
158
|
- New refusals come back as error results with their `details.reason`: `revision_mismatch` (with
|
|
119
159
|
`current_revision`), `edit_not_found` / `edit_ambiguous` (with `index`), `host_capacity` (retryable). A read of a
|
|
120
160
|
sleeping workspace its disk cannot answer (`offline_unavailable`, `offline_budget`) wakes it like any
|
|
121
161
|
`workspace_not_running`; `offline_changed` (503) is retried.
|
|
122
162
|
|
|
123
|
-
Version check (
|
|
163
|
+
Version check (needs an API that serves `GET /v1/client-versions`):
|
|
124
164
|
|
|
125
165
|
- Version check at startup: after the configuration is validated, the server asks `GET /v1/client-versions` (through
|
|
126
166
|
its own fetch, the SDK's `checkClientVersion`: one request, 3 s timeout, never awaited by the protocol) whether
|
|
@@ -141,7 +181,7 @@ Version check (contracts §30.4; needs an API that serves `GET /v1/client-versio
|
|
|
141
181
|
authenticates with a project API key, which the API refuses for account actions (403 `forbidden` on the account
|
|
142
182
|
routes, 401 on the `/v1/auth` session routes); they need a person's session (`sfu_...`), which the CLI signs in.
|
|
143
183
|
|
|
144
|
-
### send_feedback: feedback straight to the
|
|
184
|
+
### send_feedback: feedback straight to the Shardflux team (needs an API with POST /v1/feedback)
|
|
145
185
|
|
|
146
186
|
- New management tool `send_feedback` (`message`, `category`: bug, confusing, missing, idea, praise or other; optional
|
|
147
187
|
`workspace`, `request_id`, `error_code`, `command`). It calls the SDK's `sendFeedback()` with `client:
|
|
@@ -149,8 +189,8 @@ Version check (contracts §30.4; needs an API that serves `GET /v1/client-versio
|
|
|
149
189
|
`SHARDFLUX_AGENT_LABEL`); a pinned server fills `workspace` with its key. Result: `{id, received_at, duplicate,
|
|
150
190
|
note}`. Always listed (any API key may send feedback).
|
|
151
191
|
- The server instructions and the tool description ask the agent to call it actively while it works: the moment a
|
|
152
|
-
call fails unexpectedly, an error or doc is confusing, something is missing
|
|
153
|
-
when its user
|
|
192
|
+
call fails unexpectedly, an error or doc is confusing, something is missing, or it needed a workaround, and
|
|
193
|
+
when its user asks for something new (paraphrased, without private
|
|
154
194
|
data; the agent tells its user it sent feedback).
|
|
155
195
|
- Failed calls carry a `feedback` field next to `error`, except the expected flow: `invalid_arguments`,
|
|
156
196
|
`workspace_pinned`, `unauthenticated`, `validation_failed`, `timeout`, and refusals whose error names the next step
|
|
@@ -198,8 +238,7 @@ Needs `@shardflux/sdk` 0.7.0 or later. New dependency: `yaml` (template.yaml).
|
|
|
198
238
|
|
|
199
239
|
Needs `@shardflux/sdk` 0.6.2 (the workspace version).
|
|
200
240
|
|
|
201
|
-
The API no longer lets a start (open, resume, fork) wait in `capacity_pending` forever: one
|
|
202
|
-
15 minutes after it began fails with `capacity_unavailable` (retryable; nothing was started, a suspended workspace
|
|
241
|
+
The API no longer lets a start (open, resume, fork) wait in `capacity_pending` forever: one still queued 15 minutes after it began fails with `capacity_unavailable` (retryable; nothing was started, a suspended workspace
|
|
203
242
|
stays suspended).
|
|
204
243
|
|
|
205
244
|
- Failed-operation errors (`code: operation_failed`) carry `retryable`, the operation error's own flag, and
|
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ with the scoped project API key you give it. Each tool call becomes one SDK requ
|
|
|
14
14
|
|
|
15
15
|
Documentation: <https://docs.shardflux.dev/reference/mcp>.
|
|
16
16
|
|
|
17
|
-
> **
|
|
18
|
-
>
|
|
17
|
+
> **Compatibility.** The API is versioned (`/v1`). Breaking changes ship only in minor releases and are marked
|
|
18
|
+
> **Breaking** in the changelog.
|
|
19
19
|
|
|
20
20
|
Requires Node.js 24 or later and a Shardflux project API key (`sfk_...`, from the Shardflux console).
|
|
21
21
|
|
|
@@ -174,7 +174,7 @@ environment `SHARDFLUX_API_KEY`.
|
|
|
174
174
|
| `SHARDFLUX_WAKE_TIMEOUT_MS` | Longest wait per tool call for a workspace to wake or finish a transition, 1000-3600000 ms. Default 120000. It is clamped to `SHARDFLUX_MCP_TOOL_TIMEOUT_MS`, and each wake also ends 250 ms before the call's own deadline. |
|
|
175
175
|
| `SHARDFLUX_AGENT_LABEL` | Attribution label of every tool token this server obtains. Default `mcp`. It appears as an agent session in `GET /v1/workspaces/{id}/agent-sessions`, the console and `shard ws sessions`. |
|
|
176
176
|
| `SHARDFLUX_MCP_LOG_LEVEL` | `debug`, `info` (default), `warn` or `error`. Logs are JSON lines on **stderr**; stdout is the protocol. The key is never logged, and anything key-shaped is redacted. |
|
|
177
|
-
| `SHARDFLUX_HTTP_KEEPALIVE
|
|
177
|
+
| `SHARDFLUX_HTTP_KEEPALIVE` | 0.6.0+: default private HTTP/1.1 pooling on Node 26; `0` forces close for diagnosis, `1` uses native pooling. |
|
|
178
178
|
| `SHARDFLUX_NO_UPDATE_CHECK` | (0.4.0) `1`, `true`, `yes` or `on` turns off the startup version check (see "Updates"). |
|
|
179
179
|
| `NO_UPDATE_NOTIFIER` | (0.4.0) The npm convention: any non-empty value also turns the check off. |
|
|
180
180
|
|
|
@@ -189,11 +189,9 @@ working directory: the directory your MCP client starts it in.
|
|
|
189
189
|
|
|
190
190
|
## Feedback: agents are asked to use `send_feedback` while they work
|
|
191
191
|
|
|
192
|
-
**(0.4.0+)** The `send_feedback` tool sends a message straight to the Shardflux
|
|
192
|
+
**(0.4.0+)** The `send_feedback` tool sends a message straight to the Shardflux team, who read every one. The
|
|
193
193
|
server instructions and the tool description ask the agent to call it actively during its work, not once at the end:
|
|
194
|
-
the moment a call fails unexpectedly, an error or doc is confusing, something is missing or
|
|
195
|
-
workaround; when its user complains or is frustrated about Shardflux or the workflow around it (paraphrased, with what
|
|
196
|
-
they were trying to do); and when its user asked for something Shardflux could not do, or made awkward. Short and
|
|
194
|
+
the moment a call fails unexpectedly, an error or doc is confusing, something is missing, or it needed a workaround; when its user asks for a capability, an option or a smoother workflow (paraphrased, with what they were trying to do); and when its user has an idea for how Shardflux could fit their work better. Short and
|
|
197
195
|
frequent beats polished, with the `request_id` and error code from the error. The agent tells its user it sent
|
|
198
196
|
feedback, and leaves out private data about the user and secrets or code they did not mean to share (paraphrase, never
|
|
199
197
|
transcripts). Failed calls carry a ready-made suggestion in a `feedback` field (see "Contract").
|
|
@@ -239,12 +237,11 @@ The management tools:
|
|
|
239
237
|
| `workspace_status` | One workspace plus its five most recent operations. |
|
|
240
238
|
| `workspace_suspend`, `workspace_resume` | Lifecycle operation. Returns at once unless `wait: true` (then once it finished, through the SDK's own `wait`). |
|
|
241
239
|
| `workspace_suspend` with `after_seconds` | (0.4.1) Suspend when idle: the workspace is suspended once it has been idle for `after_seconds` (0-3600; 0 = as soon as it is idle, 0.5.1+), instead of now. The description tells the agent to use it when it finishes its work, so the workspace stops using RAM soon after. Its next tool call on the workspace cancels it; a running command or a keepalive postpones it. Returns `suspend_request` (`not_before`: the earliest suspend) and a `message`; `operation` when a suspend was already in progress. Not with `wait`. `workspace_status` shows a pending request as `workspace.suspend_request`. Not for file-first workspaces. |
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
240
|
+
| `workspace_suspend` with `durable: true` | (0.6.0+) Returns once the suspend is in durable storage. A suspend returns as soon as the workspace is sealed on its host, typically in a few hundred ms; its operation's `durable` turns true when the copy lands in durable storage, typically within a second (`durability` shows its progress). |
|
|
241
|
+
| `workspace_fork` | Fork into `new_key`. A fork of a running workspace carries `durable` and `durability` the same way (0.6.0+). |
|
|
242
|
+
| `operation_wait` | Keep waiting for an operation. `durable: true` (0.6.0+): a suspend or fork, until its copy is in durable storage. |
|
|
244
243
|
| `usage_summary` | The organization's usage summary for the current period: meters, allowances with their cap state, `allowance_exhausted` with `exhausted_reason`, and (0.4.1+, an API with opt-in overage) `spend_cap`: the overage state, cap, charges, lines per allowance and the date the cap is projected to be reached. |
|
|
245
|
-
|
|
246
|
-
| `usage_summary` | The organization's usage summary for the current period. |
|
|
247
|
-
| `send_feedback` | (0.4.0) Feedback straight to the Shardflux founder: `message`, `category`, and optional `workspace`, `request_id`, `error_code`, `command`. See [Feedback](#feedback-agents-are-asked-to-use-send_feedback-while-they-work). |
|
|
244
|
+
| `send_feedback` | (0.4.0) Feedback straight to the Shardflux team: `message`, `category`, and optional `workspace`, `request_id`, `error_code`, `command`. See [Feedback](#feedback-agents-are-asked-to-use-send_feedback-while-they-work). |
|
|
248
245
|
| `template_get` | (0.3.0) A template's versions with their settings (env, inputs, start commands, services, defaults); with `version`, that version's recipe in request form. |
|
|
249
246
|
| `template_languages` | (0.3.0) The languages and versions a base (`<slug>@<version>`) offers `build.languages`. |
|
|
250
247
|
| `template_build` | (0.3.0) Build a version of an organization template from a recipe v2: `recipe` (the document) or `file` (a template.yaml or .json path). File entries may name local `from` paths, uploaded first (folders as a tar). **Every local path must resolve inside the server's working directory**; others are refused before any request. Unpublished unless `publish: true`; `wait: true` follows the build within the call's deadline. |
|
|
@@ -259,24 +256,23 @@ The SDK's workspace tools come from `workspaceTools()`:
|
|
|
259
256
|
- `browser_screenshot`, `browser_content`
|
|
260
257
|
|
|
261
258
|
They are published with **the SDK's JSON Schemas verbatim**, plus `workspace_key` and, where the
|
|
262
|
-
SDK schema has none, `timeout_ms`. They are filtered by the key's tool permissions
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
exact text and, without an `expected_revision`, reads the file's revision first so a concurrent change fails the
|
|
259
|
+
SDK schema has none, `timeout_ms`. They are filtered by the key's tool permissions when the server
|
|
260
|
+
can read them; otherwise it lists every workspace tool. `exec`'s `cwd` is an absolute path (default
|
|
261
|
+
`/home/user`): a relative one is an error result with `details.reason: invalid_cwd` whose message names the absolute
|
|
262
|
+
path it likely means, and a command that could not start (a `cwd` that is not a directory, a program not on `PATH`)
|
|
263
|
+
returns `exit_code: null` with `error: { code, message, reason: "exec_failed_to_start" }` naming the workspace's
|
|
264
|
+
reason (0.4.1+). `browser_screenshot` returns an MCP image content block. `search_files` is annotated read-only;
|
|
265
|
+
`edit_file` replaces exact text and, without an `expected_revision`, reads the file's revision first so a concurrent change fails the
|
|
269
266
|
edit (`conflict`, `details.reason: revision_mismatch`) instead of being overwritten.
|
|
270
267
|
|
|
271
|
-
Each workspace tool call first sends the wake hint (`POST /wake-hint`, without waiting for it): a workspace
|
|
272
|
-
|
|
273
|
-
`list_files` and `search_files` send none (0.4.0+): a suspended workspace
|
|
268
|
+
Each workspace tool call first sends the wake hint (`POST /wake-hint`, without waiting for it): a parked workspace
|
|
269
|
+
starts restoring while the call is prepared, and a suspended one starts resuming. `read_file`,
|
|
270
|
+
`list_files` and `search_files` send none (0.4.0+): a suspended workspace is read from its saved disk,
|
|
274
271
|
listed and searched there without resuming it (the state at suspension).
|
|
275
272
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
until the workspace runs on an upgraded host.
|
|
273
|
+
If search or patches are not available for a workspace, `search_files` and `edit_file` fail with `conflict`,
|
|
274
|
+
`reason: host_feature_unavailable` (`details.feature` `file_search` or `file_patch`), `retryable: false` and a `hint`
|
|
275
|
+
naming the fallback (`exec` with `grep`; `read_file` then `write_file`).
|
|
280
276
|
|
|
281
277
|
Deleting a workspace is deliberately not exposed to agents; use the
|
|
282
278
|
[CLI](https://www.npmjs.com/package/@shardflux/cli) or the console.
|
|
@@ -295,13 +291,13 @@ the state of the template's start commands and services (`failed` names the step
|
|
|
295
291
|
|
|
296
292
|
### File-first workspaces (0.4.0)
|
|
297
293
|
|
|
298
|
-
`workspace_open` with `mode: "file_first"` (or `SHARDFLUX_WORKSPACE_MODE=file_first`) opens a file-first workspace
|
|
299
|
-
|
|
294
|
+
`workspace_open` with `mode: "file_first"` (or `SHARDFLUX_WORKSPACE_MODE=file_first`) opens a file-first workspace:
|
|
295
|
+
a versioned file tree under /home/user with no VM between calls. It is ready at once and never
|
|
300
296
|
suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_revision`.
|
|
301
297
|
|
|
302
298
|
- `exec` runs each command as an execution: a fresh VM on the workspace's files. Only files under /home/user persist
|
|
303
299
|
between calls. The result adds `execution_id`, `state`, `tree_revision` and `changed` (the paths the command added,
|
|
304
|
-
modified or deleted, up to 200, with `changed_truncated`). An execution
|
|
300
|
+
modified or deleted, up to 200, with `changed_truncated`). An execution runs to completion: when the call's
|
|
305
301
|
deadline passes first, the error carries its `execution_id` and the execution continues server side.
|
|
306
302
|
- The files tools work as on a processful workspace. Each change publishes the next tree revision.
|
|
307
303
|
- The process, terminal, git and browser tools and `workspace_suspend`, `workspace_resume`, `workspace_fork` and
|
|
@@ -311,17 +307,17 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
311
307
|
`notifications/tools/list_changed`. An unpinned server lists every tool; calling one the named workspace's mode
|
|
312
308
|
lacks returns an error with `reason: "not_supported_for_mode"` and a `hint`, before any request.
|
|
313
309
|
- Errors of file-first workspaces carry `reason` and a `hint`: `not_supported_for_mode`, `mode_mismatch` (a key's
|
|
314
|
-
mode never changes), `mode_not_available` (the
|
|
310
|
+
mode never changes), `mode_not_available` (the account does not have file-first workspaces), `layout_unsupported`
|
|
315
311
|
(a legacy template), `tree_revision_mismatch` (with `current_tree_revision`), `outside_tree_root`,
|
|
316
|
-
`execution_in_progress` (another execution holds the workspace), `execution_id_reused` and `no_execution_host`
|
|
317
|
-
|
|
312
|
+
`execution_in_progress` (another execution holds the workspace), `execution_id_reused` and `no_execution_host`
|
|
313
|
+
(the execution cannot be placed right now; retried with the same id; nothing ran).
|
|
318
314
|
|
|
319
315
|
```json
|
|
320
316
|
{
|
|
321
317
|
"mcpServers": {
|
|
322
318
|
"shardflux": {
|
|
323
|
-
"command": "
|
|
324
|
-
"args": ["/
|
|
319
|
+
"command": "npx",
|
|
320
|
+
"args": ["-y", "@shardflux/mcp"],
|
|
325
321
|
"env": { "SHARDFLUX_API_KEY": "sfk_...", "SHARDFLUX_WORKSPACE_KEY": "me/agent-files", "SHARDFLUX_TEMPLATE": "python-node-browser", "SHARDFLUX_WORKSPACE_MODE": "file_first" }
|
|
326
322
|
}
|
|
327
323
|
}
|
|
@@ -343,25 +339,23 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
343
339
|
`allowance_used` (upgrade, or turn on overage), `overage_paused` (a plan payment is past due) or
|
|
344
340
|
`spend_cap_reached` (raise the spend cap or upgrade), and `details.spend_cap`. An owner or billing member acts
|
|
345
341
|
on it in the console; retrying does not help.
|
|
346
|
-
|
|
347
342
|
- **(0.4.0+)** A failed call has a `feedback` field next to `error`: a one-sentence suggestion to call
|
|
348
343
|
`send_feedback` with category `bug`, the `request_id` and the error code. Not for `invalid_arguments`,
|
|
349
344
|
`workspace_pinned` or `unauthenticated`. A failed `send_feedback` carries a `hint` instead (when to retry, or the
|
|
350
345
|
email address).
|
|
351
|
-
- **
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
never retries it itself. The server instructions say the same.
|
|
346
|
+
- **Start deadlines (0.2.1+).** A queued open, resume or fork (`capacity_pending`) has a deadline 15 minutes after
|
|
347
|
+
it was created; a wait that ends first returns `code: timeout`, and its message names the deadline. A start still
|
|
348
|
+
pending then fails: `operation_failed`, `details.error_code: capacity_unavailable`, `retryable: true`, and a
|
|
349
|
+
message telling the agent that nothing was started and it can retry. The server instructions say the same.
|
|
356
350
|
- **Timing (0.2.0+).** A call that opens a workspace or waits says where the time went, from the SDK's lifecycle
|
|
357
351
|
timing, compact for model context:
|
|
358
352
|
|
|
359
353
|
```json
|
|
360
354
|
"timing": {
|
|
361
|
-
"action": "
|
|
362
|
-
"phases": ["request
|
|
363
|
-
"server": { "queued_ms":
|
|
364
|
-
"outside_server_ms":
|
|
355
|
+
"action": "resume", "outcome": "succeeded", "operation_id": "01a0ead6-bd61-737a-ae88-66e2d01a6e25", "total_ms": 413,
|
|
356
|
+
"phases": ["request 218 ms", "queued 195 ms"],
|
|
357
|
+
"server": { "queued_ms": 51, "run_ms": 290, "total_ms": 341, "resume_path": "local_cache" },
|
|
358
|
+
"outside_server_ms": 72, "retries": 0
|
|
365
359
|
}
|
|
366
360
|
```
|
|
367
361
|
|
|
@@ -369,7 +363,7 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
369
363
|
`workspace_fork` carry it with `wait: true`. Workspace tools carry it when they had to wake the workspace
|
|
370
364
|
(`action: "wake"`). Other results do not.
|
|
371
365
|
- Phases are in order, as `phase(reason) ms`. `request(held)` means the API held the open until the workspace was
|
|
372
|
-
ready. `capacity_pending(no_ready_host)` is time
|
|
366
|
+
ready. `capacity_pending(no_ready_host)` is time queued to start.
|
|
373
367
|
- `server` is the operation's own queued, run and total time, plus the start or resume path the cell reported.
|
|
374
368
|
A resume also has `memory_restored` (0.5.0+) when the API reports it, and `cold_boot_reason` when it is false.
|
|
375
369
|
- `outside_server_ms` is everything else: network, polling, reading the workspace, the tool token.
|
|
@@ -378,7 +372,7 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
378
372
|
with the first tool token, which the next tool call reuses.
|
|
379
373
|
- **Deadlines.** Every call has one: `timeout_ms`, clamped to the ceiling.
|
|
380
374
|
- Waits time out just before the deadline with `code: timeout` and the `operation_id`; the
|
|
381
|
-
operation continues server side (a start
|
|
375
|
+
operation continues server side (a queued start until its 15-minute deadline).
|
|
382
376
|
- Any request still in flight at the deadline is aborted.
|
|
383
377
|
- `exec`'s command timeout is capped at the deadline minus 2 s, so its output comes back.
|
|
384
378
|
- **Cancellation.** MCP `notifications/cancelled` aborts the SDK work behind the call: waits,
|
|
@@ -386,7 +380,7 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
386
380
|
the server stays up. The lifecycle operation itself is not canceled.
|
|
387
381
|
- **Workspace tools wake suspended workspaces, but do not open new ones.** A workspace that was
|
|
388
382
|
never opened is `not_found`: call `workspace_open` first. Reads (`read_file`, `list_files`,
|
|
389
|
-
`search_files`) of a suspended workspace
|
|
383
|
+
`search_files`) of a suspended workspace are answered from its saved disk
|
|
390
384
|
without waking it.
|
|
391
385
|
- A suspended workspace is resumed (or the resume or open already running is joined), and the
|
|
392
386
|
call runs once it is running. A call made during a suspend or resume waits for it to finish.
|
|
@@ -399,10 +393,10 @@ suspended. Workspace results carry `mode` and, for file-first workspaces, `tree_
|
|
|
399
393
|
suspended).
|
|
400
394
|
- With `SHARDFLUX_WAKE=off` the API's refusal comes back instead (`conflict`,
|
|
401
395
|
`details.reason: workspace_not_running`, or `workspace_not_running` from the cell gateway).
|
|
402
|
-
- **
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
- **Detecting a cold resume (0.5.0+).** A resume restores memory and running processes. After a platform runtime
|
|
397
|
+
update, a resume can boot from the saved disk instead of restoring memory (a cold boot), also when a workspace
|
|
398
|
+
tool wakes it; check `memory_restored`. The call still runs with the files as of the suspend, and processes start
|
|
399
|
+
fresh. The result (or the error result) then carries
|
|
406
400
|
`timing.server.resume_path: "cold_boot"`, `memory_restored: false`, `cold_boot_reason` (e.g. `runtime_changed`)
|
|
407
401
|
and a `notice` the agent reads: to start its dev servers, databases, watchers and background jobs again. The
|
|
408
402
|
server instructions and the `workspace_resume` description say so too.
|
|
@@ -423,3 +417,10 @@ await server.connect(transport); // any @modelcontextprotocol/sdk transport
|
|
|
423
417
|
## License
|
|
424
418
|
|
|
425
419
|
Apache-2.0
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
## Integration controls (0.6.0+)
|
|
423
|
+
|
|
424
|
+
`workspace_open` accepts `labels` and `idle_policy`; `workspace_list` accepts exact label filters. `workspace_set_labels` replaces all labels. `workspace_idle` reads activity without waking, `workspace_keepalive` declares ongoing work, and `workspace_set_idle_policy` accepts adaptive, never, fixed:<seconds>, or default.
|
|
425
|
+
`workspace_exec_start` starts a tracked background command with optional `stdin_open`; `workspace_exec_input` writes data at an acknowledged byte offset and can send EOF with `close`. Both require exec permission and processful workspaces. Frames are bounded to 64 KiB; use the returned offset after partial acknowledgements. The host and guest must advertise pipe-input support.
|
|
426
|
+
`workspace_resume` recovers failed IDs without replacing their disks. A completed delete frees the key for a new workspace ID. Protocol errors include their source; a bare 404 is not a workspace tombstone.
|
package/dist/errors.d.ts
CHANGED
|
@@ -18,22 +18,22 @@ export interface ToolErrorInfo {
|
|
|
18
18
|
execution_id?: string;
|
|
19
19
|
/** tree_revision_mismatch: the revision the workspace's file tree is at (0.4.0). */
|
|
20
20
|
current_tree_revision?: number;
|
|
21
|
-
source?: 'api' | 'cell';
|
|
21
|
+
source?: 'api' | 'cell' | 'unknown';
|
|
22
22
|
details?: Record<string, unknown>;
|
|
23
23
|
issues?: string[];
|
|
24
24
|
last_state?: string;
|
|
25
|
-
/** What to do instead (0.4.0): refusals that concern a workspace's mode
|
|
25
|
+
/** What to do instead (0.4.0): refusals that concern a workspace's mode or a tool that is not available for the workspace. */
|
|
26
26
|
hint?: string;
|
|
27
27
|
}
|
|
28
28
|
export declare function redact(text: string, key?: string): string;
|
|
29
29
|
/**
|
|
30
|
-
* What the model can do about a refusal that concerns a workspace's mode
|
|
30
|
+
* What the model can do about a refusal that concerns a workspace's mode, from its
|
|
31
31
|
* details.reason and details; undefined for every other refusal.
|
|
32
32
|
*/
|
|
33
33
|
export declare function modeHint(reason: string | undefined, details?: Record<string, unknown>): string | undefined;
|
|
34
34
|
/**
|
|
35
|
-
* 409 conflict host_feature_unavailable (
|
|
36
|
-
*
|
|
35
|
+
* 409 conflict host_feature_unavailable (0.4.0): the tool is not available for this workspace. Not retryable, so the
|
|
36
|
+
* hint names the fallback that works now.
|
|
37
37
|
*/
|
|
38
38
|
export declare function hostFeatureHint(reason: string | undefined, details?: Record<string, unknown>): string | undefined;
|
|
39
39
|
export interface DescribeContext {
|
package/dist/errors.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* content is `{ "error": { code, message, ... } }` with the API's closed error
|
|
4
4
|
* code, never as protocol failures, so the calling model can react to them.
|
|
5
5
|
* API and cell refusals add `reason` (their details.reason) and, for refusals
|
|
6
|
-
* that concern a workspace's mode (file-first workspaces
|
|
7
|
-
*
|
|
6
|
+
* that concern a workspace's mode (file-first workspaces) or a
|
|
7
|
+
* tool that is not available for the workspace (host_feature_unavailable), a
|
|
8
8
|
* `hint` saying what to do instead.
|
|
9
9
|
*/
|
|
10
|
-
import { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateUploadError, ToolArgumentError, TreeRevisionMismatchError } from '@shardflux/sdk';
|
|
10
|
+
import { DurabilityLostError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateUploadError, ToolArgumentError, TreeRevisionMismatchError } from '@shardflux/sdk';
|
|
11
11
|
/** A refusal decided by this server (argument/pinning problems, unknown workspace key). */
|
|
12
12
|
export class ToolError extends Error {
|
|
13
13
|
name = 'ToolError';
|
|
@@ -31,7 +31,7 @@ const str = (v) => (typeof v === 'string' && v !== '' ? v : undefined);
|
|
|
31
31
|
/** The tools a file-first workspace has, for hints. */
|
|
32
32
|
const FILE_FIRST_TOOLS_TEXT = 'Run commands with exec and use read_file, write_file, list_files, search_files and edit_file for files; only files under /home/user persist between exec calls.';
|
|
33
33
|
/**
|
|
34
|
-
* What the model can do about a refusal that concerns a workspace's mode
|
|
34
|
+
* What the model can do about a refusal that concerns a workspace's mode, from its
|
|
35
35
|
* details.reason and details; undefined for every other refusal.
|
|
36
36
|
*/
|
|
37
37
|
export function modeHint(reason, details = {}) {
|
|
@@ -52,7 +52,7 @@ export function modeHint(reason, details = {}) {
|
|
|
52
52
|
: "A workspace's mode never changes: open this key with the mode it was created with, or use another key.";
|
|
53
53
|
}
|
|
54
54
|
case 'mode_not_available':
|
|
55
|
-
return 'This deployment does not offer file-first workspaces
|
|
55
|
+
return 'This deployment does not offer file-first workspaces: open the workspace with mode "processful" (or without mode).';
|
|
56
56
|
case 'layout_unsupported':
|
|
57
57
|
return mode === 'file_first'
|
|
58
58
|
? 'File-first workspaces run on layered template versions and this one is not: pick another template (template_get lists its versions and disk_layouts) or open with mode "processful".'
|
|
@@ -66,26 +66,26 @@ export function modeHint(reason, details = {}) {
|
|
|
66
66
|
case 'execution_id_reused':
|
|
67
67
|
return 'That execution id was already used for a different command; call exec again (each call uses a new execution id).';
|
|
68
68
|
case 'no_execution_host':
|
|
69
|
-
return '
|
|
69
|
+
return 'The execution cannot be placed right now and nothing ran; retry in a few seconds.';
|
|
70
70
|
default:
|
|
71
71
|
return undefined;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* 409 conflict host_feature_unavailable (
|
|
76
|
-
*
|
|
75
|
+
* 409 conflict host_feature_unavailable (0.4.0): the tool is not available for this workspace. Not retryable, so the
|
|
76
|
+
* hint names the fallback that works now.
|
|
77
77
|
*/
|
|
78
78
|
export function hostFeatureHint(reason, details = {}) {
|
|
79
79
|
if (reason !== 'host_feature_unavailable')
|
|
80
80
|
return undefined;
|
|
81
|
-
const
|
|
81
|
+
const retry = 'Retrying does not help.';
|
|
82
82
|
switch (details.feature) {
|
|
83
83
|
case 'file_search':
|
|
84
|
-
return `
|
|
84
|
+
return `search_files is not available for this workspace. Search with exec instead, e.g. grep -rn -- PATTERN PATH (add -i to ignore case, -E for a regular expression). ${retry}`;
|
|
85
85
|
case 'file_patch':
|
|
86
|
-
return `
|
|
86
|
+
return `edit_file is not available for this workspace. Use read_file, change the text, then write_file with the whole new content. ${retry}`;
|
|
87
87
|
default:
|
|
88
|
-
return `This
|
|
88
|
+
return `This tool is not available for this workspace. ${retry}`;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
export function describeToolError(err, context = {}) {
|
|
@@ -101,7 +101,7 @@ export function describeToolError(err, context = {}) {
|
|
|
101
101
|
retryable: err.retryable,
|
|
102
102
|
source: err.source,
|
|
103
103
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
104
|
-
// No execution_id: a refusal answers before anything ran, and the execution id stays unused
|
|
104
|
+
// No execution_id: a refusal answers before anything ran, and the execution id stays unused.
|
|
105
105
|
...(err instanceof TreeRevisionMismatchError && err.currentTreeRevision !== null ? { current_tree_revision: err.currentTreeRevision } : {}),
|
|
106
106
|
...(err.details ? { details: err.details } : {}),
|
|
107
107
|
...(hint ? { hint } : {}),
|
|
@@ -112,21 +112,32 @@ export function describeToolError(err, context = {}) {
|
|
|
112
112
|
if (err instanceof ToolError)
|
|
113
113
|
return { code: err.code, message: err.message, ...(err.details ? { details: err.details } : {}) };
|
|
114
114
|
if (err instanceof OperationTimeoutError) {
|
|
115
|
+
if (err.durable) {
|
|
116
|
+
return { code: 'timeout', message: `${err.message} Call operation_wait with this operation_id and durable: true to keep waiting.`, operation_id: err.operationId, last_state: err.lastState, retryable: true, details: { durable: false, state: 'pending' } };
|
|
117
|
+
}
|
|
115
118
|
return { code: 'timeout', message: `${err.message} Call operation_wait with this operation_id to keep waiting.`, operation_id: err.operationId, last_state: err.lastState, retryable: true };
|
|
116
119
|
}
|
|
120
|
+
// 0.6.0: the suspend (or fork) succeeded; its durable copy could not be made (durability.state lost).
|
|
121
|
+
if (err instanceof DurabilityLostError) {
|
|
122
|
+
return { code: 'durability_lost', message: err.message, operation_id: err.operationId, retryable: false, details: { state: 'lost', ...(err.durability.reason ? { reason: err.durability.reason } : {}), ...(err.durability.checkpointId ? { checkpoint_id: err.durability.checkpointId } : {}) } };
|
|
123
|
+
}
|
|
117
124
|
if (err instanceof OperationFailedError) {
|
|
118
|
-
// retryable: the operation error's own flag. capacity_unavailable (
|
|
119
|
-
//
|
|
125
|
+
// retryable: the operation error's own flag. capacity_unavailable (the start passed its deadline) is retryable:
|
|
126
|
+
// nothing was started, so the agent may send it again.
|
|
120
127
|
const opDetails = err.operation.error?.details;
|
|
121
128
|
const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
|
|
122
129
|
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
|
|
123
|
-
const hint = err.errorCode === 'capacity_unavailable'
|
|
130
|
+
const hint = err.errorCode === 'capacity_unavailable'
|
|
131
|
+
? '. The start passed its deadline and nothing was started; send it again.'
|
|
132
|
+
: err.workspaceActive
|
|
133
|
+
? '. The workspace was in use, so the suspend-when-idle was canceled; nothing changed and it keeps running.'
|
|
134
|
+
: '';
|
|
124
135
|
return { code: 'operation_failed', message: `${err.message}${hint}`, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}) };
|
|
125
136
|
}
|
|
126
137
|
if (named(err, 'TimeoutError')) {
|
|
127
|
-
// A file-first execution
|
|
138
|
+
// A file-first execution runs to completion server side and then publishes what it changed.
|
|
128
139
|
const continues = context.executionId
|
|
129
|
-
? `; execution ${context.executionId} continues server side (a file-first execution
|
|
140
|
+
? `; execution ${context.executionId} continues server side (a file-first execution runs to completion): the files it changes are published when it ends, and the next exec or file change on this workspace waits for it`
|
|
130
141
|
: context.operationId
|
|
131
142
|
? '; the operation continues server side (use operation_wait)'
|
|
132
143
|
: '';
|
|
@@ -141,7 +152,7 @@ export function describeToolError(err, context = {}) {
|
|
|
141
152
|
if (err instanceof TemplateUploadError)
|
|
142
153
|
return { code: 'upload_failed', message: err.message, ...(err.status ? { status: err.status } : {}), details: { sha256: err.sha256, ...(err.code ? { storage_code: err.code } : {}) } };
|
|
143
154
|
if (err instanceof ShardfluxProtocolError)
|
|
144
|
-
return { code: 'protocol_error', message: err.message, status: err.status, ...(context.executionId ? { execution_id: context.executionId } : {}) };
|
|
155
|
+
return { code: 'protocol_error', message: err.message, status: err.status, source: err.source, ...(context.executionId ? { execution_id: context.executionId } : {}) };
|
|
145
156
|
if (err instanceof TypeError && err.message === 'fetch failed') {
|
|
146
157
|
const cause = err.cause;
|
|
147
158
|
return {
|
package/dist/http.d.ts
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fetch given to the SDK: the runtime's fetch with `Connection: close` on
|
|
3
|
-
* every request (API and cell gateway).
|
|
4
|
-
*
|
|
5
|
-
* Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
|
|
6
|
-
* sent on a reused keep-alive connection until an unrelated timer fires (up to
|
|
7
|
-
* ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
|
|
8
|
-
* with bare fetch against both node:http and Fastify servers; Node 22 (undici
|
|
9
|
-
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
|
-
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
|
-
* Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
|
|
12
|
-
*/
|
|
1
|
+
/** Share the SDK's pooled transport. A caller-supplied fetch stays under the caller's control. */
|
|
13
2
|
export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch): typeof fetch;
|
package/dist/http.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
|
|
6
|
-
* sent on a reused keep-alive connection until an unrelated timer fires (up to
|
|
7
|
-
* ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
|
|
8
|
-
* with bare fetch against both node:http and Fastify servers; Node 22 (undici
|
|
9
|
-
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
|
-
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
|
-
* Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
|
|
12
|
-
*/
|
|
13
|
-
export function makeFetch(env, base = fetch) {
|
|
14
|
-
if (env.SHARDFLUX_HTTP_KEEPALIVE === '1')
|
|
15
|
-
return base;
|
|
16
|
-
return (input, init) => {
|
|
17
|
-
const headers = new Headers(init?.headers);
|
|
18
|
-
headers.set('connection', 'close');
|
|
19
|
-
return base(input, { ...init, headers });
|
|
20
|
-
};
|
|
1
|
+
import { defaultFetch } from '@shardflux/sdk';
|
|
2
|
+
/** Share the SDK's pooled transport. A caller-supplied fetch stays under the caller's control. */
|
|
3
|
+
export function makeFetch(env, base = defaultFetch(env)) {
|
|
4
|
+
return base;
|
|
21
5
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
3
3
|
import type { ClientVersionStatus, JsonSchema, LifecycleTiming, Operation, TemplateBuild, TemplateDetail, TemplateOwner, ToolName, WorkspaceMode, WorkspaceTool, WorkspaceView } from '@shardflux/sdk';
|
|
4
4
|
import type { McpConfig } from './config.js';
|
|
5
5
|
import type { ToolErrorInfo } from './errors.js';
|
|
6
|
-
export declare const MCP_SERVER_VERSION = "0.
|
|
7
|
-
/** The package this server is distributed as: its entry in GET /v1/client-versions
|
|
6
|
+
export declare const MCP_SERVER_VERSION = "0.6.0";
|
|
7
|
+
/** The package this server is distributed as: its entry in GET /v1/client-versions. */
|
|
8
8
|
export declare const MCP_PACKAGE = "@shardflux/mcp";
|
|
9
9
|
export declare const ALL_TOOL_PERMISSIONS: readonly ToolName[];
|
|
10
10
|
export type ObjectSchema = JsonSchema & {
|
|
@@ -36,6 +36,33 @@ export declare function summarizeWorkspace(v: WorkspaceView): {
|
|
|
36
36
|
version: number;
|
|
37
37
|
};
|
|
38
38
|
active_operation: {
|
|
39
|
+
lost_suspend?: ({
|
|
40
|
+
checkpoint_id: string;
|
|
41
|
+
generation_id?: string;
|
|
42
|
+
reason?: string;
|
|
43
|
+
suspended_at?: string;
|
|
44
|
+
restored_checkpoint_id?: string;
|
|
45
|
+
state_as_of?: string;
|
|
46
|
+
} & {
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}) | undefined;
|
|
49
|
+
durability?: ({
|
|
50
|
+
state: "pending" | "durable" | "lost";
|
|
51
|
+
checkpoint_id?: string;
|
|
52
|
+
generation_id?: string;
|
|
53
|
+
local_commit_at?: string;
|
|
54
|
+
durable_by?: string;
|
|
55
|
+
durable_at?: string;
|
|
56
|
+
local_commit_to_durable_ms?: number;
|
|
57
|
+
overdue_at?: string;
|
|
58
|
+
reason?: string;
|
|
59
|
+
code?: string;
|
|
60
|
+
recovery_point_checkpoint_id?: string | null;
|
|
61
|
+
} & {
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}) | undefined;
|
|
64
|
+
suspend_path?: string | undefined;
|
|
65
|
+
durable?: boolean | undefined;
|
|
39
66
|
error?: {
|
|
40
67
|
[key: string]: unknown;
|
|
41
68
|
} | undefined;
|
|
@@ -62,6 +89,9 @@ export declare function summarizeWorkspace(v: WorkspaceView): {
|
|
|
62
89
|
purpose: "standard" | "template_draft" | "template_test";
|
|
63
90
|
disk_layout: "legacy" | "layered";
|
|
64
91
|
idle_timeout_seconds: number | null;
|
|
92
|
+
labels: {
|
|
93
|
+
[key: string]: string;
|
|
94
|
+
};
|
|
65
95
|
ended_reason: "closed" | "idle_timeout" | "draft_discarded" | null;
|
|
66
96
|
/** Start commands and services of the template version; null when it has none. */
|
|
67
97
|
startup: {
|
|
@@ -171,6 +201,33 @@ export declare function summarizeTemplate(t: TemplateDetail): {
|
|
|
171
201
|
versions_truncated: boolean;
|
|
172
202
|
};
|
|
173
203
|
export declare function summarizeOperation(o: Operation): {
|
|
204
|
+
lost_suspend?: ({
|
|
205
|
+
checkpoint_id: string;
|
|
206
|
+
generation_id?: string;
|
|
207
|
+
reason?: string;
|
|
208
|
+
suspended_at?: string;
|
|
209
|
+
restored_checkpoint_id?: string;
|
|
210
|
+
state_as_of?: string;
|
|
211
|
+
} & {
|
|
212
|
+
[key: string]: unknown;
|
|
213
|
+
}) | undefined;
|
|
214
|
+
durability?: ({
|
|
215
|
+
state: "pending" | "durable" | "lost";
|
|
216
|
+
checkpoint_id?: string;
|
|
217
|
+
generation_id?: string;
|
|
218
|
+
local_commit_at?: string;
|
|
219
|
+
durable_by?: string;
|
|
220
|
+
durable_at?: string;
|
|
221
|
+
local_commit_to_durable_ms?: number;
|
|
222
|
+
overdue_at?: string;
|
|
223
|
+
reason?: string;
|
|
224
|
+
code?: string;
|
|
225
|
+
recovery_point_checkpoint_id?: string | null;
|
|
226
|
+
} & {
|
|
227
|
+
[key: string]: unknown;
|
|
228
|
+
}) | undefined;
|
|
229
|
+
suspend_path?: string | undefined;
|
|
230
|
+
durable?: boolean | undefined;
|
|
174
231
|
error?: {
|
|
175
232
|
[key: string]: unknown;
|
|
176
233
|
} | undefined;
|
|
@@ -185,13 +242,15 @@ export declare function summarizeOperation(o: Operation): {
|
|
|
185
242
|
/**
|
|
186
243
|
* The SDK's LifecycleTiming, compact enough for model context: where an open, a wait or a wake spent its time. Phases
|
|
187
244
|
* are `phase(reason) ms` in order (`request(held)`: the server held the open; `capacity_pending(no_ready_host)`:
|
|
188
|
-
*
|
|
245
|
+
* queued); `server` is the operation's own queued/run/total time; `outside_server_ms` is the rest
|
|
189
246
|
* (network, polling, view and token).
|
|
190
247
|
*/
|
|
191
248
|
export declare function compactTiming(t: LifecycleTiming): {
|
|
192
249
|
total_ms: number;
|
|
193
250
|
phases: string[];
|
|
194
251
|
server: {
|
|
252
|
+
durability_state?: "pending" | "durable" | "lost" | undefined;
|
|
253
|
+
durable?: boolean | undefined;
|
|
195
254
|
cold_boot_reason?: string | undefined;
|
|
196
255
|
memory_restored?: boolean | undefined;
|
|
197
256
|
resume_path?: string | undefined;
|
|
@@ -213,6 +272,13 @@ export declare function compactTiming(t: LifecycleTiming): {
|
|
|
213
272
|
* and background jobs it started are gone. Undefined for any other timing, including one whose memory_restored is unknown.
|
|
214
273
|
*/
|
|
215
274
|
export declare function processRestartNotice(t: LifecycleTiming): string | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* (0.6.0) The `notice` of a result whose resume restored an earlier checkpoint because the workspace's latest suspend
|
|
277
|
+
* could not be kept (`lost_suspend`): the files and processes are as of that checkpoint.
|
|
278
|
+
*/
|
|
279
|
+
export declare function lostSuspendNotice(t: LifecycleTiming): string | undefined;
|
|
280
|
+
/** The notices of a timing (cold boot, lost suspend), joined; undefined when there are none. */
|
|
281
|
+
export declare function timingNotice(t: LifecycleTiming): string | undefined;
|
|
216
282
|
export declare function okResult(value: unknown): CallToolResult;
|
|
217
283
|
/**
|
|
218
284
|
* `timing` (optional): where the failed open/wait/wake spent its time, next to `error`. `extra` (0.4.0): more sibling
|
|
@@ -239,7 +305,7 @@ export interface ServerOptions {
|
|
|
239
305
|
versionCheck?: boolean;
|
|
240
306
|
}
|
|
241
307
|
/**
|
|
242
|
-
* The startup version check (0.4.0
|
|
308
|
+
* The startup version check (0.4.0): one GET <api>/v1/client-versions through the server's fetch
|
|
243
309
|
* (the SDK's checkClientVersion: 3 s timeout, never throws) for @shardflux/mcp at MCP_SERVER_VERSION. Outdated or
|
|
244
310
|
* unsupported: one `warn` line whose message is the notice (`@shardflux/mcp 0.4.0 is outdated: 0.5.0 is available.
|
|
245
311
|
* Update: <upgrade command>`) plus the status fields. Anything else (current; unknown: not listed, `latest` null while
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - templates (0.3.0): template_get (versions, settings, a version's recipe), template_languages and
|
|
9
9
|
* template_build (a recipe v2 object or a template.yaml path inside the
|
|
10
10
|
* server's working directory; local `from` paths are uploaded by the SDK);
|
|
11
|
-
* - send_feedback (0.4.0): feedback straight to the Shardflux
|
|
11
|
+
* - send_feedback (0.4.0): feedback straight to the Shardflux team; the instructions, its description and
|
|
12
12
|
* the `feedback` field of error results ask agents to use it while they work;
|
|
13
13
|
* - the SDK's workspace tools (`workspaceTools()`: exec, files, processes,
|
|
14
14
|
* terminal, git, browser), published with the SDK's own JSON Schemas plus
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
* call first sends the wake hint (the SDK tool runner's `workspace.hint()`,
|
|
22
22
|
* fire-and-forget) so a parked workspace restores while the call is prepared,
|
|
23
23
|
* except read_file, list_files and search_files, which a sleeping workspace
|
|
24
|
-
* answers from its disk without waking
|
|
24
|
+
* answers from its disk without waking. Each call has a deadline
|
|
25
25
|
* (`timeout_ms`, clamped to SHARDFLUX_MCP_TOOL_TIMEOUT_MS) and MCP cancellation
|
|
26
26
|
* (notifications/cancelled -> `extra.signal`) aborts the underlying SDK
|
|
27
27
|
* request or wait. Failures come back as `isError` tool results carrying the
|
|
28
28
|
* API error code. stdout is the protocol; logs go to stderr. Opens, waits and
|
|
29
29
|
* wakes carry the SDK's lifecycle timing, compacted (`compactTiming`).
|
|
30
30
|
*
|
|
31
|
-
* File-first workspaces (0.4.0
|
|
31
|
+
* File-first workspaces (0.4.0: `workspace_open` `mode:
|
|
32
32
|
* "file_first"`) have files and executions only. A pinned server lists the
|
|
33
33
|
* tools of its workspace's mode (the key's mode once it resolves, else
|
|
34
34
|
* SHARDFLUX_WORKSPACE_MODE, else processful) and sends
|
|
@@ -53,8 +53,8 @@ import { parse as parseYaml } from 'yaml';
|
|
|
53
53
|
import { DEFAULT_WAKE_TIMEOUT_MS } from "./config.js";
|
|
54
54
|
import { ToolError, describeToolError, redact } from "./errors.js";
|
|
55
55
|
import { makeFetch } from "./http.js";
|
|
56
|
-
export const MCP_SERVER_VERSION = '0.
|
|
57
|
-
/** The package this server is distributed as: its entry in GET /v1/client-versions
|
|
56
|
+
export const MCP_SERVER_VERSION = '0.6.0';
|
|
57
|
+
/** The package this server is distributed as: its entry in GET /v1/client-versions. */
|
|
58
58
|
export const MCP_PACKAGE = '@shardflux/mcp';
|
|
59
59
|
const USER_AGENT = `shardflux-mcp/${MCP_SERVER_VERSION} shardflux-sdk-ts/${SDK_VERSION}`;
|
|
60
60
|
export const ALL_TOOL_PERMISSIONS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
|
|
@@ -75,7 +75,7 @@ export function sdkToolDefinitions(tools = ALL_TOOL_PERMISSIONS, mode = 'process
|
|
|
75
75
|
return workspaceTools(noWorkspace, { tools: [...tools], mode });
|
|
76
76
|
}
|
|
77
77
|
/** Management tools that need a workspace VM or an operation: not listed for a pinned file-first workspace. */
|
|
78
|
-
const VM_ONLY_MANAGEMENT = new Set(['workspace_suspend', 'workspace_resume', 'workspace_fork', 'operation_wait']);
|
|
78
|
+
const VM_ONLY_MANAGEMENT = new Set(['workspace_suspend', 'workspace_resume', 'workspace_fork', 'operation_wait', 'workspace_idle', 'workspace_keepalive', 'workspace_set_idle_policy', 'workspace_exec_start', 'workspace_exec_input']);
|
|
79
79
|
/** What to use instead of a workspace tool a file-first workspace does not have, by the tool's permission. */
|
|
80
80
|
const FILE_FIRST_INSTEAD = {
|
|
81
81
|
process: 'Nothing runs between exec calls: run the program, and whatever needs it, within one exec command.',
|
|
@@ -132,6 +132,7 @@ export function summarizeWorkspace(v) {
|
|
|
132
132
|
purpose: v.purpose,
|
|
133
133
|
disk_layout: v.disk_layout,
|
|
134
134
|
idle_timeout_seconds: v.idle_timeout_seconds,
|
|
135
|
+
labels: v.labels,
|
|
135
136
|
ended_reason: v.ended_reason,
|
|
136
137
|
/** Start commands and services of the template version; null when it has none. */
|
|
137
138
|
startup: v.startup ?? null,
|
|
@@ -194,6 +195,11 @@ export function summarizeOperation(o) {
|
|
|
194
195
|
created_at: o.created_at,
|
|
195
196
|
completed_at: o.completed_at ?? null,
|
|
196
197
|
...(o.error ? { error: o.error } : {}),
|
|
198
|
+
// 0.6.0: suspend and fork say whether their copy is in durable storage; a resume names a suspend it could not restore.
|
|
199
|
+
...(typeof o.result?.durable === 'boolean' ? { durable: o.result.durable } : {}),
|
|
200
|
+
...(o.result?.suspend_path ? { suspend_path: o.result.suspend_path } : {}),
|
|
201
|
+
...(o.result?.durability ? { durability: o.result.durability } : {}),
|
|
202
|
+
...(o.result?.lost_suspend ? { lost_suspend: o.result.lost_suspend } : {}),
|
|
197
203
|
};
|
|
198
204
|
}
|
|
199
205
|
function isObject(v) {
|
|
@@ -202,7 +208,7 @@ function isObject(v) {
|
|
|
202
208
|
/**
|
|
203
209
|
* The SDK's LifecycleTiming, compact enough for model context: where an open, a wait or a wake spent its time. Phases
|
|
204
210
|
* are `phase(reason) ms` in order (`request(held)`: the server held the open; `capacity_pending(no_ready_host)`:
|
|
205
|
-
*
|
|
211
|
+
* queued); `server` is the operation's own queued/run/total time; `outside_server_ms` is the rest
|
|
206
212
|
* (network, polling, view and token).
|
|
207
213
|
*/
|
|
208
214
|
export function compactTiming(t) {
|
|
@@ -225,6 +231,8 @@ export function compactTiming(t) {
|
|
|
225
231
|
// 0.5.0: false when the resume booted the saved disk (processes restarted); absent when the API did not say.
|
|
226
232
|
...(typeof s.memoryRestored === 'boolean' ? { memory_restored: s.memoryRestored } : {}),
|
|
227
233
|
...(s.coldBootReason ? { cold_boot_reason: s.coldBootReason } : {}),
|
|
234
|
+
...(typeof s.durable === 'boolean' ? { durable: s.durable } : {}),
|
|
235
|
+
...(s.durability ? { durability_state: s.durability.state } : {}),
|
|
228
236
|
}
|
|
229
237
|
: null,
|
|
230
238
|
outside_server_ms: ms(t.outsideServerMs),
|
|
@@ -248,6 +256,23 @@ export function processRestartNotice(t) {
|
|
|
248
256
|
return (`The workspace was resumed from its saved disk, not from memory${why}. Its files are as they were when it was suspended, ` +
|
|
249
257
|
'but every process was restarted, as after a reboot: start again any dev server, database, watcher or background job you had running, and open new terminals.');
|
|
250
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* (0.6.0) The `notice` of a result whose resume restored an earlier checkpoint because the workspace's latest suspend
|
|
261
|
+
* could not be kept (`lost_suspend`): the files and processes are as of that checkpoint.
|
|
262
|
+
*/
|
|
263
|
+
export function lostSuspendNotice(t) {
|
|
264
|
+
const l = t.server?.lostSuspend;
|
|
265
|
+
if (!l)
|
|
266
|
+
return undefined;
|
|
267
|
+
return (`The workspace was resumed from its checkpoint${l.stateAsOf ? ` of ${l.stateAsOf}` : ''} (${l.restoredCheckpointId ?? 'the previous one'}); ` +
|
|
268
|
+
`its suspend${l.suspendedAt ? ` at ${l.suspendedAt}` : ''} was not kept${l.reason ? ` (${l.reason})` : ''}. ` +
|
|
269
|
+
'Work done between those times is not in the workspace: check the files you changed and run again what you need.');
|
|
270
|
+
}
|
|
271
|
+
/** The notices of a timing (cold boot, lost suspend), joined; undefined when there are none. */
|
|
272
|
+
export function timingNotice(t) {
|
|
273
|
+
const parts = [processRestartNotice(t), lostSuspendNotice(t)].filter((x) => x !== undefined);
|
|
274
|
+
return parts.length ? parts.join(' ') : undefined;
|
|
275
|
+
}
|
|
251
276
|
export function okResult(value) {
|
|
252
277
|
if (isObject(value) && value.mime_type === 'image/png' && typeof value.data_base64 === 'string') {
|
|
253
278
|
const { data_base64: data, ...meta } = value;
|
|
@@ -268,7 +293,7 @@ const FEEDBACK_EMAIL = 'shardflux@heliosone.fi';
|
|
|
268
293
|
* When to use send_feedback, for the agent: the same words as `shard feedback --help`, in the server instructions and
|
|
269
294
|
* the tool description. It has to make an agent actually call it while it works.
|
|
270
295
|
*/
|
|
271
|
-
const FEEDBACK_USE = 'Call it actively while you work, not once at the end. Send a short note the moment: something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround; your user
|
|
296
|
+
const FEEDBACK_USE = 'Call it actively while you work, not once at the end. Send a short note the moment: something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround; your user asks for a capability, an option or a smoother workflow: paraphrase it, with what they were trying to do; your user has an idea for how Shardflux could fit their work better. Short and frequent beats polished; include the request_id and error code (failed calls carry a ready-made "feedback" field). No need to ask first, but tell your user you sent it. Leave out private data about the user and secrets or code they did not mean to share; paraphrase, never paste transcripts.';
|
|
272
297
|
/**
|
|
273
298
|
* No send_feedback suggestion for the expected flow: argument, configuration or credential problems of the call itself,
|
|
274
299
|
* a wait that gave up while the work continues, and refusals whose error names the exact next step (the same list as
|
|
@@ -302,18 +327,18 @@ const INSTRUCTIONS = [
|
|
|
302
327
|
'Call workspace_open first (it creates the workspace on first use and reconnects afterwards, never resetting it), then use exec, read_file, write_file and the other workspace tools with the same workspace_key.',
|
|
303
328
|
'Lifecycle calls return operations; operation_wait keeps waiting. Workspace tools resume a suspended workspace on use (read_file, list_files and search_files read its disk without resuming it when they can); if that takes too long the error (code timeout) names the operation to pass to operation_wait. Errors are tool results with error.code from the Shardflux API.',
|
|
304
329
|
'When you finish your work on a workspace, call workspace_suspend with after_seconds (e.g. 60): it is suspended once idle that long, so it stops using RAM; your next tool call on it cancels that.',
|
|
305
|
-
'A start (open, resume, fork)
|
|
330
|
+
'A queued start (open, resume, fork) has a deadline 15 minutes after it was created; past it, it fails with code operation_failed, details.error_code capacity_unavailable and retryable true. Nothing was started, so send it again. retryable false means retrying will not help.',
|
|
306
331
|
'Opens, waits and wakes add timing (phases, server queued/run time) saying where the time went.',
|
|
307
332
|
'A resume (also the automatic one when a tool call finds the workspace suspended) normally restores memory and running processes. If a result carries notice and timing.server.memory_restored false (resume_path cold_boot), the workspace booted from its saved disk instead: files are kept, but every process was restarted, so start your dev servers and background jobs again.',
|
|
308
333
|
'Templates: template_get shows a template’s versions and settings (the inputs workspace_open takes); template_languages lists what a base offers build.languages; template_build builds a new version from a recipe v2 (unpublished unless publish is true).',
|
|
309
334
|
'search_files finds text in files under a directory; edit_file replaces exact text in a file (each old_text must occur once) and returns the new revision, which you can pass as expected_revision to the next edit_file of that file so a change made by someone else is detected.',
|
|
310
335
|
'File-first workspaces (workspace_open mode "file_first"; results show mode and tree_revision) keep only files: there is no VM between calls, each exec runs in a fresh VM, and only files under /home/user persist between exec calls (install dependencies there, e.g. a virtualenv, and start servers within the command that needs them).',
|
|
311
|
-
'An exec result on a file-first workspace carries the paths it changed (changed) and the new tree_revision; an execution
|
|
336
|
+
'An exec result on a file-first workspace carries the paths it changed (changed) and the new tree_revision; an execution runs to completion. Process, terminal, git and browser tools and workspace_suspend, workspace_resume and workspace_fork do not apply to them (error reason not_supported_for_mode).',
|
|
312
337
|
'Account-level actions are not tools of this server: it works inside one project with a project API key, which the API refuses for them. Registering, signing in, organizations, projects, API keys, members, billing and plan upgrades, spend alerts and audit export are done with the shard CLI: `npx @shardflux/cli@latest --help` (for example `shard auth login`, `shard setup`, `shard billing upgrade <plan>`). A person still opens the verification email and pays on the Checkout page the CLI prints.',
|
|
313
|
-
`Feedback: send_feedback goes straight to the Shardflux
|
|
338
|
+
`Feedback: send_feedback goes straight to the Shardflux team, who read every message. ${FEEDBACK_USE}`,
|
|
314
339
|
].join(' ');
|
|
315
340
|
/**
|
|
316
|
-
* The startup version check (0.4.0
|
|
341
|
+
* The startup version check (0.4.0): one GET <api>/v1/client-versions through the server's fetch
|
|
317
342
|
* (the SDK's checkClientVersion: 3 s timeout, never throws) for @shardflux/mcp at MCP_SERVER_VERSION. Outdated or
|
|
318
343
|
* unsupported: one `warn` line whose message is the notice (`@shardflux/mcp 0.4.0 is outdated: 0.5.0 is available.
|
|
319
344
|
* Update: <upgrade command>`) plus the status fields. Anything else (current; unknown: not listed, `latest` null while
|
|
@@ -421,7 +446,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
421
446
|
* Wake on use for a workspace's tools: `workspace.wake()` within the SDK's transition budget (SHARDFLUX_WAKE_TIMEOUT_MS)
|
|
422
447
|
* and 250 ms short of the call's deadline, so a wake that runs out reports the operation (OperationTimeoutError) rather
|
|
423
448
|
* than a bare deadline. null when SHARDFLUX_WAKE=off: the refusal (workspace_not_running) comes back instead. The
|
|
424
|
-
* wake's held resume
|
|
449
|
+
* wake's held resume returns the token of this server's tools (its agent label), so the woken call
|
|
425
450
|
* runs at once with it.
|
|
426
451
|
*/
|
|
427
452
|
const wakeFor = (ws) => config.wake === false
|
|
@@ -480,7 +505,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
480
505
|
/**
|
|
481
506
|
* The workspace a key names, across every lifetime and purpose (sessions, drafts and test instances are hidden from
|
|
482
507
|
* the default list), preferring the live workspace over tombstones: an ended session leaves a tombstone with the same
|
|
483
|
-
* key, and the key then opens a new workspace
|
|
508
|
+
* key, and the key then opens a new workspace. Null when the key names none.
|
|
484
509
|
*/
|
|
485
510
|
const lookup = async (key, opts2) => {
|
|
486
511
|
const cached = handles.get(key);
|
|
@@ -544,7 +569,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
544
569
|
{
|
|
545
570
|
name: 'workspace_open',
|
|
546
571
|
title: 'Open workspace',
|
|
547
|
-
description: 'Open a persistent workspace by key: creates it from the template on first use, reconnects (or resumes) it afterwards, never resets it. Waits until it is ready unless wait is false; on timeout the start continues server side (operation_wait)
|
|
572
|
+
description: 'Open a persistent workspace by key: creates it from the template on first use, reconnects (or resumes) it afterwards, never resets it. Waits until it is ready unless wait is false; on timeout the start continues server side (operation_wait); a queued start has a deadline 15 minutes after it was created. The result’s timing says where the time went. mode "file_first" opens a file-first workspace (files only, ready at once; see mode).',
|
|
548
573
|
inputSchema: obj({
|
|
549
574
|
workspace_key: workspaceKeyProp(pinned),
|
|
550
575
|
template: {
|
|
@@ -554,6 +579,8 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
554
579
|
description: config.template ? `Template slug (default "${config.template}").` : 'Template slug for a new workspace, e.g. "python-node-browser".',
|
|
555
580
|
},
|
|
556
581
|
...capsProps,
|
|
582
|
+
labels: { type: 'object', additionalProperties: true, description: 'Searchable workspace labels; replaces existing labels when supplied.' },
|
|
583
|
+
idle_policy: { type: 'string', pattern: '^(adaptive|never|fixed:[0-9]+)$', description: 'Automatic suspend policy.' },
|
|
557
584
|
lifetime: {
|
|
558
585
|
type: 'string',
|
|
559
586
|
enum: ['persistent', 'session'],
|
|
@@ -590,6 +617,8 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
590
617
|
...(caps ? { caps } : {}),
|
|
591
618
|
...(lifetime ? { lifetime } : {}),
|
|
592
619
|
...(inputs ? { inputs } : {}),
|
|
620
|
+
...(args.labels !== undefined ? { labels: stringMap(args.labels, 'labels') } : {}),
|
|
621
|
+
...(typeof args.idle_policy === 'string' ? { idlePolicy: args.idle_policy } : {}),
|
|
593
622
|
...(mode ? { mode } : {}),
|
|
594
623
|
agentLabel: config.agentLabel,
|
|
595
624
|
wait: args.wait === false ? false : waitOpts(call),
|
|
@@ -605,6 +634,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
605
634
|
description: 'List the workspaces of this API key’s project (key, state, template, active operation).',
|
|
606
635
|
inputSchema: obj({
|
|
607
636
|
prefix: { type: 'string', minLength: 1, maxLength: 200, description: 'Only keys starting with this prefix.' },
|
|
637
|
+
labels: { type: 'object', additionalProperties: true, description: 'Exact label matches; all supplied pairs must match.' },
|
|
608
638
|
state: { type: 'string', enum: OBSERVED_STATES, description: 'Observed state filter.' },
|
|
609
639
|
include_deleted: { type: 'boolean', description: 'Include deleted workspaces (and ended sessions).' },
|
|
610
640
|
lifetime: { type: 'string', enum: ['persistent', 'session', 'any'], description: 'Lifetime filter (default persistent: session workspaces are hidden).' },
|
|
@@ -616,6 +646,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
616
646
|
run: async (args) => {
|
|
617
647
|
const page = await cloud.workspaces.list({
|
|
618
648
|
...(typeof args.prefix === 'string' ? { keyPrefix: args.prefix } : {}),
|
|
649
|
+
...(args.labels !== undefined ? { labels: stringMap(args.labels, 'labels') } : {}),
|
|
619
650
|
...(typeof args.state === 'string' ? { state: args.state } : {}),
|
|
620
651
|
...(args.include_deleted === true ? { includeDeleted: true } : {}),
|
|
621
652
|
...(typeof args.lifetime === 'string' ? { lifetime: args.lifetime } : {}),
|
|
@@ -641,10 +672,49 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
641
672
|
return { workspace: summarizeWorkspace(ws.data), recent_operations: ops.data.map(summarizeOperation) };
|
|
642
673
|
},
|
|
643
674
|
},
|
|
675
|
+
{
|
|
676
|
+
name: 'workspace_idle', title: 'Inspect idle status', description: 'Read idle policy and activity without waking or recording activity.',
|
|
677
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned) }, keyRequired),
|
|
678
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
679
|
+
run: async (args, call) => (await resolve(keyOf(args), { signal: call.signal })).idle(call.signal),
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: 'workspace_keepalive', title: 'Declare ongoing work', description: 'Prevent idle suspension for seconds; never shortens an existing keepalive.',
|
|
683
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned), seconds: { type: 'integer', minimum: 1 } }, [...keyRequired, 'seconds']),
|
|
684
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
685
|
+
run: async (args, call) => (await resolve(keyOf(args), { signal: call.signal })).keepalive(args.seconds, call.signal),
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'workspace_set_idle_policy', title: 'Set idle policy', description: 'Set adaptive, never or fixed:<seconds> (60..604800); default restores the inherited policy.',
|
|
689
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned), policy: { type: 'string', description: 'adaptive, never, fixed:<seconds>, or default' } }, [...keyRequired, 'policy']),
|
|
690
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
691
|
+
run: async (args, call) => summarizeWorkspace((await (await resolve(keyOf(args), { signal: call.signal })).setIdlePolicy(args.policy === 'default' ? null : args.policy)).data),
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: 'workspace_set_labels', title: 'Replace workspace labels', description: 'Replace all labels; an empty object clears them. Labels are searchable metadata, not secrets.',
|
|
695
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned), labels: { type: 'object', additionalProperties: true } }, [...keyRequired, 'labels']),
|
|
696
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
697
|
+
run: async (args, call) => summarizeWorkspace((await (await resolve(keyOf(args), { signal: call.signal })).setLabels(stringMap(args.labels, 'labels'))).data),
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: 'workspace_exec_start', permission: 'exec', title: 'Start background command', description: 'Start a tracked exec session. stdin_open keeps a pipe open for workspace_exec_input. Processful workspaces only.',
|
|
701
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned), argv: { type: 'array', items: { type: 'string' }, minItems: 1 }, session_id: { type: 'string' }, stdin_open: { type: 'boolean' }, cwd: { type: 'string' } }, [...keyRequired, 'argv']),
|
|
702
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
703
|
+
run: async (args, call) => (await resolve(keyOf(args), { signal: call.signal })).cell().exec.start({ argv: args.argv,
|
|
704
|
+
...(typeof args.session_id === 'string' ? { session_id: args.session_id } : {}), stdin_open: args.stdin_open === true,
|
|
705
|
+
...(typeof args.cwd === 'string' ? { cwd: args.cwd } : {}) }, call.signal),
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: 'workspace_exec_input', permission: 'exec', title: 'Write command stdin', description: 'Write up to 64 KiB at the acknowledged byte offset (0 initially). close sends EOF. A repeated identical last frame cannot duplicate bytes; continue from a partial acknowledgement.',
|
|
709
|
+
inputSchema: obj({ workspace_key: workspaceKeyProp(pinned), session_id: { type: 'string' }, data: { type: 'string', maxLength: 65536 }, offset: { type: 'integer', minimum: 0 }, close: { type: 'boolean' } }, [...keyRequired, 'session_id', 'offset']),
|
|
710
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
711
|
+
run: async (args, call) => (await resolve(keyOf(args), { signal: call.signal })).cell().exec.input(args.session_id, typeof args.data === 'string' ? args.data : '', { offset: args.offset, close: args.close === true, signal: call.signal }),
|
|
712
|
+
},
|
|
644
713
|
{
|
|
645
714
|
name: 'workspace_suspend',
|
|
646
715
|
title: 'Suspend workspace',
|
|
647
|
-
description: 'Suspend a running workspace (
|
|
716
|
+
description: 'Suspend a running workspace (full-state checkpoint; processes stop, files and state are kept, and the next tool call resumes it). Returns the suspend operation (with wait: once finished, and its timing). ' +
|
|
717
|
+
'A finished suspend returns as soon as the workspace is sealed on its host (typically a few hundred ms); its durable field turns true when the copy lands in durable storage, typically within a second. Pass durable: true to return only then. ' +
|
|
648
718
|
'With after_seconds the suspend is deferred instead: the workspace is suspended once it has been idle that long. Use it when you finish your work (the end of your turn), e.g. after_seconds 60, so the workspace stops using RAM soon after instead of waiting for its idle timeout. ' +
|
|
649
719
|
'Your next tool call on the workspace cancels it; a command still running or a keepalive postpones it until after_seconds after it ends. Returns suspend_request (not_before: the earliest suspend). Not for file-first workspaces (never suspended).',
|
|
650
720
|
inputSchema: obj({
|
|
@@ -656,10 +726,14 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
656
726
|
description: 'Suspend once the workspace has been idle this many seconds (0-3600; 0 = as soon as it is idle) instead of now. Omit to suspend now.',
|
|
657
727
|
},
|
|
658
728
|
wait: { type: 'boolean', description: 'Wait until the suspend finishes (default false). Not with after_seconds.' },
|
|
729
|
+
durable: { type: 'boolean', description: 'Wait until the suspend is in durable storage (result durable: true; implies wait). Not with after_seconds.' },
|
|
659
730
|
}, keyRequired),
|
|
660
731
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
661
732
|
run: async (args, call) => {
|
|
662
733
|
const afterSeconds = typeof args.after_seconds === 'number' ? args.after_seconds : undefined;
|
|
734
|
+
if (afterSeconds !== undefined && args.durable === true) {
|
|
735
|
+
throw new ToolError('invalid_arguments', 'durable does not apply with after_seconds: the suspend happens later, once the workspace has been idle. Omit durable, or omit after_seconds to suspend now.');
|
|
736
|
+
}
|
|
663
737
|
if (afterSeconds !== undefined && args.wait === true) {
|
|
664
738
|
throw new ToolError('invalid_arguments', 'wait does not apply with after_seconds: the suspend happens later, once the workspace has been idle. Omit wait, or omit after_seconds to suspend now.');
|
|
665
739
|
}
|
|
@@ -669,7 +743,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
669
743
|
throw notForMode(ws, 'suspend', 'api', `workspace_suspend is not available for the file-first workspace "${key}": nothing runs between its exec calls, so it is never suspended; its files persist as they are.`);
|
|
670
744
|
}
|
|
671
745
|
if (afterSeconds === undefined) {
|
|
672
|
-
const op = await cloud.workspaces.suspend(ws.id, lifecycleOpts(args, call));
|
|
746
|
+
const op = await cloud.workspaces.suspend(ws.id, args.durable === true ? { ...lifecycleOpts({ ...args, wait: true }, call), durable: true } : lifecycleOpts(args, call));
|
|
673
747
|
call.operationId = op.id;
|
|
674
748
|
return { operation: summarizeOperation(op) };
|
|
675
749
|
}
|
|
@@ -687,7 +761,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
687
761
|
},
|
|
688
762
|
},
|
|
689
763
|
lifecycleTool('workspace_resume', 'Resume workspace', 'Resume a suspended workspace. Returns the resume operation (with wait: once finished, and its timing). A notice in the result (timing.server.memory_restored false) means it booted from its saved disk: files kept, processes restarted. Not for file-first workspaces (never suspended).', 'resume',
|
|
690
|
-
// Through the server's handle: a waited resume is held until the workspace runs
|
|
764
|
+
// Through the server's handle: a waited resume is held until the workspace runs and the handle
|
|
691
765
|
// keeps the view and this server's tool token, so the next workspace tool starts at once.
|
|
692
766
|
(ws, o) => ws.resume({ ...o, agentLabel: config.agentLabel })),
|
|
693
767
|
{
|
|
@@ -716,10 +790,19 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
716
790
|
{
|
|
717
791
|
name: 'operation_wait',
|
|
718
792
|
title: 'Wait for operation',
|
|
719
|
-
description: 'Wait for a lifecycle operation (open, suspend, resume, fork, ...) to finish, up to timeout_ms. Returns the operation and the wait’s timing.',
|
|
720
|
-
inputSchema: obj({
|
|
793
|
+
description: 'Wait for a lifecycle operation (open, suspend, resume, fork, ...) to finish, up to timeout_ms. Returns the operation and the wait’s timing. With durable (a suspend or fork), also wait until its copy is in durable storage.',
|
|
794
|
+
inputSchema: obj({
|
|
795
|
+
operation_id: { type: 'string', minLength: 36, maxLength: 36, description: 'Operation id (UUID).' },
|
|
796
|
+
durable: { type: 'boolean', description: 'A suspend or fork: also wait until result durable is true.' },
|
|
797
|
+
}, ['operation_id']),
|
|
721
798
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
722
|
-
run: async (args, call) =>
|
|
799
|
+
run: async (args, call) => {
|
|
800
|
+
if (args.durable !== true)
|
|
801
|
+
return { operation: summarizeOperation(await wait(call, String(args.operation_id))) };
|
|
802
|
+
call.timed = true;
|
|
803
|
+
call.operationId = String(args.operation_id);
|
|
804
|
+
return { operation: summarizeOperation(await cloud.workspaces.waitForDurable(String(args.operation_id), waitOpts(call))) };
|
|
805
|
+
},
|
|
723
806
|
},
|
|
724
807
|
{
|
|
725
808
|
name: 'template_get',
|
|
@@ -838,13 +921,13 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
838
921
|
management.push({
|
|
839
922
|
name: 'send_feedback',
|
|
840
923
|
title: 'Send feedback',
|
|
841
|
-
description: `Send feedback straight to the Shardflux
|
|
924
|
+
description: `Send feedback straight to the Shardflux team, who read every message. ${FEEDBACK_USE} Categories: bug (something failed or behaved wrongly), confusing (an error, doc, name or output was unclear or misleading), missing (a capability, option or template you needed does not exist), idea, praise (something worked well), other. Rate limited per API key (rate_limited with details.retry_after_seconds); the same message within 24 hours is recorded once (duplicate: true).`,
|
|
842
925
|
inputSchema: obj({
|
|
843
926
|
message: { type: 'string', minLength: 1, maxLength: FEEDBACK_MESSAGE_MAX_LENGTH, description: 'What you did, what happened and what you expected (1-8000 characters). Leave secrets out.' },
|
|
844
927
|
category: { type: 'string', enum: [...FEEDBACK_CATEGORIES], description: 'bug, confusing, missing, idea, praise or other.' },
|
|
845
928
|
workspace: { type: 'string', minLength: 1, maxLength: 200, description: pinned ? `Workspace key or id it is about (default "${pinned}").` : 'Workspace key or id it is about.' },
|
|
846
|
-
request_id: { type: 'string', minLength: 1, maxLength: 200, description: 'request_id from the error, so the
|
|
847
|
-
error_code: { type: 'string', minLength: 1, maxLength: 100, description: 'The error code seen (error.code, or details.error_code of a failed operation), e.g.
|
|
929
|
+
request_id: { type: 'string', minLength: 1, maxLength: 200, description: 'request_id from the error, so the team can find the logs.' },
|
|
930
|
+
error_code: { type: 'string', minLength: 1, maxLength: 100, description: 'The error code seen (error.code, or details.error_code of a failed operation), e.g. template_not_found.' },
|
|
848
931
|
command: { type: 'string', minLength: 1, maxLength: 2000, description: 'The tool call (name and arguments) or command that led to it.' },
|
|
849
932
|
}, ['message', 'category']),
|
|
850
933
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
@@ -870,7 +953,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
870
953
|
id: r.id,
|
|
871
954
|
received_at: r.receivedAt,
|
|
872
955
|
duplicate: r.duplicate,
|
|
873
|
-
note: r.duplicate ? 'The same message was already received in the last 24 hours; it was not emailed again.' : 'Delivered to the Shardflux
|
|
956
|
+
note: r.duplicate ? 'The same message was already received in the last 24 hours; it was not emailed again.' : 'Delivered to the Shardflux team. Keep sending feedback as you work.',
|
|
874
957
|
};
|
|
875
958
|
},
|
|
876
959
|
});
|
|
@@ -883,7 +966,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
883
966
|
*/
|
|
884
967
|
const execDescription = (sdkDescription, listing) => {
|
|
885
968
|
if (listing === 'file_first')
|
|
886
|
-
return `${sdkDescription}${execDeadline} An execution
|
|
969
|
+
return `${sdkDescription}${execDeadline} An execution runs to completion: if the deadline passes first it continues server side (the error names its execution_id) and the next exec waits for it.`;
|
|
887
970
|
if (listing === 'processful')
|
|
888
971
|
return `${sdkDescription}${execDeadline}`;
|
|
889
972
|
return `${sdkDescription} On a file-first workspace (workspace_open mode "file_first") each call instead runs in a fresh VM on the workspace’s files: only files under /home/user persist between calls, and the result adds execution_id, state, tree_revision and changed (the paths the command added, modified or deleted).${execDeadline}`;
|
|
@@ -921,7 +1004,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
921
1004
|
}
|
|
922
1005
|
const ws = await resolve(key, { signal: call.signal });
|
|
923
1006
|
// The SDK's tools for the workspace's mode: a file-first workspace has exec (as executions) and the file tools.
|
|
924
|
-
// A file-first execution
|
|
1007
|
+
// A file-first execution runs to completion; its id is kept so a deadline error can name it.
|
|
925
1008
|
const tool = workspaceTools(ws, {
|
|
926
1009
|
agentLabel: config.agentLabel,
|
|
927
1010
|
tools: [...ALL_TOOL_PERMISSIONS],
|
|
@@ -982,7 +1065,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
982
1065
|
if (mode !== undefined)
|
|
983
1066
|
listedMode = mode;
|
|
984
1067
|
const fileFirst = mode === 'file_first';
|
|
985
|
-
const mgmt =
|
|
1068
|
+
const mgmt = management.filter((d) => (!fileFirst || !VM_ONLY_MANAGEMENT.has(d.name)) && (d.permission === undefined || permitted.includes(d.permission)));
|
|
986
1069
|
const tools = (fileFirst ? fileFirstDefs : workspaceDefs).filter((d) => d.permission !== undefined && permitted.includes(d.permission));
|
|
987
1070
|
return { tools: [...mgmt, ...tools].map(toMcpTool) };
|
|
988
1071
|
});
|
|
@@ -1015,7 +1098,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
1015
1098
|
try {
|
|
1016
1099
|
const value = await Promise.race([running, aborted]);
|
|
1017
1100
|
log('info', 'tool call', { tool: name, outcome: 'ok', ms: Date.now() - started });
|
|
1018
|
-
const notice = call.timed && call.timing ?
|
|
1101
|
+
const notice = call.timed && call.timing ? timingNotice(call.timing) : undefined;
|
|
1019
1102
|
return okResult(call.timed && call.timing && isObject(value) ? { ...value, timing: compactTiming(call.timing), ...(notice ? { notice } : {}) } : value);
|
|
1020
1103
|
}
|
|
1021
1104
|
catch (err) {
|
|
@@ -1045,7 +1128,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
1045
1128
|
...(info.request_id ? { request_id: info.request_id } : {}),
|
|
1046
1129
|
...(info.execution_id ? { execution_id: info.execution_id } : {}),
|
|
1047
1130
|
});
|
|
1048
|
-
// A failure suggests send_feedback; a failed send_feedback says how to still reach the
|
|
1131
|
+
// A failure suggests send_feedback; a failed send_feedback says how to still reach the Shardflux team.
|
|
1049
1132
|
const s = info.details?.retry_after_seconds;
|
|
1050
1133
|
const siblings = name === 'send_feedback'
|
|
1051
1134
|
? info.code === 'invalid_arguments'
|
|
@@ -1055,7 +1138,7 @@ export function createShardfluxMcpServer(config, opts = {}) {
|
|
|
1055
1138
|
? { feedback: feedbackSuggestion(info) }
|
|
1056
1139
|
: {};
|
|
1057
1140
|
// A wake that booted the saved disk before the failure still restarted every process: said next to the error.
|
|
1058
|
-
const notice = call.timed && call.timing ?
|
|
1141
|
+
const notice = call.timed && call.timing ? timingNotice(call.timing) : undefined;
|
|
1059
1142
|
return errorResult(info, call.timed && call.timing ? compactTiming(call.timing) : undefined, { ...siblings, ...(notice ? { notice } : {}) });
|
|
1060
1143
|
}
|
|
1061
1144
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "dev.shardflux/mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Shardflux MCP server (stdio): give Claude, Cursor, Codex or any MCP client persistent cloud workspaces to run commands, edit files, use git and a browser in. Uses your scoped Shardflux API key.",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
47
47
|
"yaml": "2.9.1",
|
|
48
48
|
"zod": "4.6.5",
|
|
49
|
-
"@shardflux/sdk": "^0.
|
|
49
|
+
"@shardflux/sdk": "^0.12.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@eslint/js": "10.0.1",
|