@aipoch/open-science 1.0.2 → 1.0.4
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/CLI.md +117 -0
- package/browser-import.d.ts +11 -5
- package/browser-import.mjs +81 -55
- package/cli.mjs +68 -8
- package/index.d.ts +1 -0
- package/index.mjs +49 -0
- package/mcp.mjs +310 -0
- package/package.json +10 -6
package/CLI.md
CHANGED
|
@@ -606,6 +606,43 @@ turns only and is rejected while root-agent, subagent, or Notebook work is activ
|
|
|
606
606
|
does not block the update. Stale revisions fail with `session_revision_conflict`; invalid or
|
|
607
607
|
unavailable references fail with `invalid_configuration`.
|
|
608
608
|
|
|
609
|
+
## Session package export and import
|
|
610
|
+
|
|
611
|
+
Export a Session to a `.science` package at an explicit path. The Session must be idle:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
open-science session export <session-id> --project <id-or-name> --output ~/backups/session.science
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`--project` resolves a Project by id or name and selects which Session to export. `--output` must
|
|
618
|
+
be an absolute or relative path ending in `.science`; when it is omitted the command fails instead
|
|
619
|
+
of opening a native save dialog, because the CLI runs without a desktop session. The command
|
|
620
|
+
returns the saved status and written path as JSON with `--json`.
|
|
621
|
+
|
|
622
|
+
Both package commands allow up to one hour by default. Use `--timeout-ms <milliseconds>`
|
|
623
|
+
to adjust the request deadline for large packages or slower disks.
|
|
624
|
+
|
|
625
|
+
Import a `.science` package. Import is non-interactive by design and requires an explicit
|
|
626
|
+
confirmation flag:
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
open-science session import ~/backups/session.science --new-project "Restored session" --non-interactive
|
|
630
|
+
open-science session import ~/backups/session.science --project <id-or-name> --non-interactive
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
`--non-interactive` confirms the import preview on the caller's behalf; without it the command
|
|
634
|
+
fails rather than waiting for an approval that a headless caller can never supply. Choose exactly
|
|
635
|
+
one destination: `--project` imports into an existing Project, `--new-project` creates one with
|
|
636
|
+
the given name, and omitting both uses the package's own Project name in a new Project. The
|
|
637
|
+
package file is read directly from disk, so no upload or drag-and-drop step is involved.
|
|
638
|
+
|
|
639
|
+
Both commands dispatch the same `sessions:export-package`, `sessions:import-package`, and
|
|
640
|
+
`sessions:package-operation` application commands the desktop app uses, over the authenticated
|
|
641
|
+
loopback HTTP channel. They are unavailable to remote Web sessions by design: the channel is
|
|
642
|
+
installed as a rejecting stub for remote callers, and the handler additionally requires a local
|
|
643
|
+
caller, so arbitrary-path file read and write stay confined to loopback callers that hold the
|
|
644
|
+
service token.
|
|
645
|
+
|
|
609
646
|
## Agent routing settings
|
|
610
647
|
|
|
611
648
|
Inspect or atomically update the global Agent framework plus Reviewer and Subagent model routing:
|
|
@@ -812,3 +849,83 @@ older releases. To switch a file credential back to OS storage, restart in OS mo
|
|
|
812
849
|
replace it with the key while the vault is available. Existing unreadable records remain intact.
|
|
813
850
|
These commands do not migrate historical configuration or store diagnostic history. Older backends that
|
|
814
851
|
lack the endpoints return an endpoint error; the CLI never falls back to editing Settings files directly.
|
|
852
|
+
|
|
853
|
+
## External agents through MCP
|
|
854
|
+
|
|
855
|
+
Start the selected profile normally, then let an MCP client launch the stdio adapter:
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
open-science start --no-open --config-root /absolute/path/to/profile
|
|
859
|
+
open-science mcp --config-root /absolute/path/to/profile
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
Example client configuration (replace paths with your installation and profile paths):
|
|
863
|
+
|
|
864
|
+
```json
|
|
865
|
+
{
|
|
866
|
+
"mcpServers": {
|
|
867
|
+
"open-science": {
|
|
868
|
+
"command": "/absolute/path/to/open-science",
|
|
869
|
+
"args": ["mcp", "--config-root", "/absolute/path/to/profile"]
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
`--profile` is an alias for `--config-root`; existing environment overrides and discovery order
|
|
876
|
+
apply. For a source checkout, use `node` as the command and put the absolute path to
|
|
877
|
+
`packages/open-science/cli.mjs` before `mcp` in `args`. The adapter connects to an already
|
|
878
|
+
running authenticated local service; it does not start it, install anything or open a browser.
|
|
879
|
+
`open-science mcp --help` works without a running service.
|
|
880
|
+
|
|
881
|
+
Tool discovery works even when the service is stopped. Calls return an actionable error until
|
|
882
|
+
it is available, and subsequent calls rediscover the service. Tool names consistently use
|
|
883
|
+
lowercase `snake_case`; argument names retain the SDK's `camelCase`. Tools accept explicit IDs
|
|
884
|
+
rather than resolving project names. Each tool publishes its description, input schema, output
|
|
885
|
+
schema and behavior annotations through `tools/list`.
|
|
886
|
+
|
|
887
|
+
| Tool | Purpose |
|
|
888
|
+
| ------------------------------ | -------------------------------------------------------- |
|
|
889
|
+
| `doctor` | Check research readiness |
|
|
890
|
+
| `list_runtimes` | List detected Agent runtimes |
|
|
891
|
+
| `list_projects` | List projects and IDs |
|
|
892
|
+
| `create_project` | Create a persistent project |
|
|
893
|
+
| `update_project` | Update project metadata and Agent Context |
|
|
894
|
+
| `list_sessions` | List sessions, optionally by project |
|
|
895
|
+
| `get_session` | Read session information/status, not a full transcript |
|
|
896
|
+
| `get_session_configuration` | Read session configuration and revision |
|
|
897
|
+
| `update_session_configuration` | Update settings with `expectedRevision` |
|
|
898
|
+
| `start_run` | Submit a prompt and return a run ID |
|
|
899
|
+
| `get_run` | Read progress, output and attention state |
|
|
900
|
+
| `cancel_run` | Explicitly cancel a run without undoing previous effects |
|
|
901
|
+
| `get_plan` | Read the plan and its version/revision |
|
|
902
|
+
| `respond_plan` | Approve/reject a reviewed version or request changes |
|
|
903
|
+
| `list_artifacts` | List artifact metadata without downloading files |
|
|
904
|
+
|
|
905
|
+
A typical workflow calls `list_projects`, `start_run` with `{ "projectId": "…", "prompt": "…" }`,
|
|
906
|
+
then polls `get_run` with the returned run ID and calls `list_artifacts` with the session ID.
|
|
907
|
+
Successful calls return `structuredContent: { "result": ... }` plus the same JSON in text
|
|
908
|
+
content. Tool execution failures set `isError: true`; exception bodies and credentials are not
|
|
909
|
+
forwarded. After a timeout or transport failure, inspect current state before repeating a write:
|
|
910
|
+
the original operation may already have succeeded. The adapter never retries mutations itself.
|
|
911
|
+
|
|
912
|
+
Runs use the service's configured framework and authorization defaults. `start_run` accepts
|
|
913
|
+
existing session, provider/model, permission, skill, specialist, compute-host, memory, review,
|
|
914
|
+
delegation and plan-first options; it does not change the global framework. A run can require
|
|
915
|
+
human action in Open-Science. Explicit `permissionPrompts: "none"` denies unresolved human
|
|
916
|
+
interactions using the existing capability check; it does not expand permissions and cannot
|
|
917
|
+
be combined with `turnIntent: "plan-first"`. Plan approval/rejection requires the version and
|
|
918
|
+
revision from `get_plan`. Configuration updates require the revision from
|
|
919
|
+
`get_session_configuration`.
|
|
920
|
+
|
|
921
|
+
MCP access grants the listed operations for the selected profile under its existing permissions.
|
|
922
|
+
The adapter is not a separate sandbox or a read-only view. Project/configuration changes, plan
|
|
923
|
+
responses and research runs persist through existing business owners and may write files or
|
|
924
|
+
incur model costs. No new database schema, historical-data migration, business status enum,
|
|
925
|
+
credential store or background service is introduced. There is no arbitrary shell/RPC tool,
|
|
926
|
+
credential-management tool, file import/export/download-to-path tool or remote HTTP MCP endpoint.
|
|
927
|
+
|
|
928
|
+
Standard output carries only MCP protocol messages. Closing stdin or sending SIGINT/SIGTERM
|
|
929
|
+
closes the adapter and its pending network requests; it neither stops the service nor cancels
|
|
930
|
+
accepted runs. MCP request cancellation likewise stops waiting, not the persisted operation.
|
|
931
|
+
Use `cancel_run` explicitly to cancel research.
|
package/browser-import.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export function checkBrowserImportAvailability(
|
|
|
36
36
|
export type BrowserImportTransfer = {
|
|
37
37
|
/** Contains a secret; never log or persist it. Browser policy may require a fresh user click. */
|
|
38
38
|
readonly url: string
|
|
39
|
+
/** Abort requests and timers, invalidate this transfer, and best-effort cancel the receiver. */
|
|
40
|
+
cancel(): Promise<void>
|
|
39
41
|
/** Acknowledges receipt only, independently of destination selection and final import consent.
|
|
40
42
|
* Authenticated desktop cancellation throws code 'cancelled', terminal for this transfer.
|
|
41
43
|
* Keep the Blob and explicitly prepare a new transfer; never automatically retry cancellation. */
|
|
@@ -46,16 +48,20 @@ export type BrowserImportTransfer = {
|
|
|
46
48
|
onPhase?: (phase: BrowserImportPhase) => void
|
|
47
49
|
/** Acknowledged bytes only; first callback follows the first successful chunk acknowledgment. */
|
|
48
50
|
onProgress?: (progress: BrowserImportProgress) => void
|
|
49
|
-
/** Default attempts preserves the 1–
|
|
51
|
+
/** Default attempts preserves the 1–30 attempt limit. wait-for-launch retries until the
|
|
50
52
|
* overall deadline or cancellation; it observes only this transfer's authenticated handshake. */
|
|
51
53
|
readinessMode?: BrowserImportReadinessMode
|
|
52
54
|
/** Readiness only; defaults to 120,000 ms. Capped at 120,000 ms in wait-for-launch,
|
|
53
|
-
* 300,000 ms in attempts mode.
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
* 300,000 ms in attempts mode. Set null only in attempts mode to stop by attempt count
|
|
56
|
+
* without an overall deadline (including across page suspension). Per-request timeouts
|
|
57
|
+
* still apply. Timeout/exhaustion permits another send() on the same live transfer. */
|
|
58
|
+
readyTimeoutMs?: number | null
|
|
59
|
+
/** 1–30 attempts with 1,000 ms gaps; defaults to 5. Ignored in wait-for-launch mode. */
|
|
56
60
|
readinessAttempts?: number
|
|
61
|
+
/** Delay after each failed attempt; defaults to 1,000 ms. Range: 0–30,000 ms. */
|
|
62
|
+
readinessIntervalMs?: number
|
|
57
63
|
/** Includes response body reads; defaults to 2,000 ms. Accepts up to 30,000 ms,
|
|
58
|
-
* clamped to 2,000 ms in wait-for-launch mode. Failed attempts
|
|
64
|
+
* clamped to 2,000 ms in wait-for-launch mode. Failed attempts use readinessIntervalMs. */
|
|
59
65
|
attemptTimeoutMs?: number
|
|
60
66
|
}): Promise<{ received: true }>
|
|
61
67
|
}
|
package/browser-import.mjs
CHANGED
|
@@ -178,9 +178,33 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
178
178
|
let sending = false
|
|
179
179
|
let received = false
|
|
180
180
|
let cancelled
|
|
181
|
+
const lifetime = new AbortController()
|
|
182
|
+
let cancellation
|
|
183
|
+
// Cancel this secret-bound receiver even while readiness is still pending.
|
|
184
|
+
const cancel = () => {
|
|
185
|
+
if (received) return Promise.resolve()
|
|
186
|
+
if (cancellation) return cancellation
|
|
187
|
+
cancelled = new BrowserImportError('cancelled', 'The transfer was cancelled.')
|
|
188
|
+
lifetime.abort(cancelled)
|
|
189
|
+
cancellation = bounded(undefined, 2000, async (signal) => {
|
|
190
|
+
const headers = await authorize('DELETE', '')
|
|
191
|
+
signal.throwIfAborted()
|
|
192
|
+
await fetch(base, {
|
|
193
|
+
method: 'DELETE',
|
|
194
|
+
mode: 'cors',
|
|
195
|
+
credentials: 'omit',
|
|
196
|
+
redirect: 'error',
|
|
197
|
+
headers,
|
|
198
|
+
signal,
|
|
199
|
+
targetAddressSpace: 'loopback'
|
|
200
|
+
})
|
|
201
|
+
}).catch(() => undefined)
|
|
202
|
+
return cancellation
|
|
203
|
+
}
|
|
181
204
|
|
|
182
205
|
return Object.freeze({
|
|
183
206
|
url,
|
|
207
|
+
cancel,
|
|
184
208
|
async send({
|
|
185
209
|
signal,
|
|
186
210
|
onProgress,
|
|
@@ -188,18 +212,23 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
188
212
|
readinessMode = 'attempts',
|
|
189
213
|
readyTimeoutMs = 120_000,
|
|
190
214
|
readinessAttempts = 5,
|
|
215
|
+
readinessIntervalMs = 1000,
|
|
191
216
|
attemptTimeoutMs = 2000
|
|
192
217
|
} = {}) {
|
|
193
218
|
if (!['attempts', 'wait-for-launch'].includes(readinessMode))
|
|
194
219
|
throw new Error('Invalid readiness mode.')
|
|
195
220
|
const waitingForLaunch = readinessMode === 'wait-for-launch'
|
|
196
|
-
|
|
221
|
+
const countOnly = !waitingForLaunch && readyTimeoutMs === null
|
|
222
|
+
if (!countOnly && (!Number.isFinite(readyTimeoutMs) || readyTimeoutMs <= 0))
|
|
197
223
|
throw new Error('Invalid receiver timeout.')
|
|
198
224
|
if (
|
|
199
225
|
(!waitingForLaunch &&
|
|
200
226
|
(!Number.isInteger(readinessAttempts) ||
|
|
201
227
|
readinessAttempts < 1 ||
|
|
202
|
-
readinessAttempts >
|
|
228
|
+
readinessAttempts > 30)) ||
|
|
229
|
+
!Number.isFinite(readinessIntervalMs) ||
|
|
230
|
+
readinessIntervalMs < 0 ||
|
|
231
|
+
readinessIntervalMs > 30_000 ||
|
|
203
232
|
!Number.isFinite(attemptTimeoutMs) ||
|
|
204
233
|
attemptTimeoutMs <= 0 ||
|
|
205
234
|
attemptTimeoutMs > 30_000
|
|
@@ -208,49 +237,54 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
208
237
|
if (sending) throw new Error('This browser transfer is already running.')
|
|
209
238
|
if (cancelled) throw cancelled
|
|
210
239
|
if (received) return { received: true }
|
|
211
|
-
signal
|
|
240
|
+
signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal
|
|
241
|
+
signal.throwIfAborted()
|
|
212
242
|
sending = true
|
|
213
243
|
let verified = false
|
|
214
|
-
const request = async (path, init = {}) =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
[
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
if (response.status === 410) {
|
|
227
|
-
const value = await response.json().catch(() => {
|
|
228
|
-
throw new BrowserImportError(
|
|
229
|
-
'incompatible',
|
|
230
|
-
'Could not verify the local receiver identity.'
|
|
231
|
-
)
|
|
244
|
+
const request = async (path, init = {}) =>
|
|
245
|
+
bounded(signal, 30_000, async (requestSignal) => {
|
|
246
|
+
const response = await fetch(base + path, {
|
|
247
|
+
mode: 'cors',
|
|
248
|
+
credentials: 'omit',
|
|
249
|
+
cache: 'no-store',
|
|
250
|
+
redirect: 'error',
|
|
251
|
+
targetAddressSpace: 'loopback',
|
|
252
|
+
...init,
|
|
253
|
+
signal: AbortSignal.any([requestSignal, init.signal].filter(Boolean))
|
|
232
254
|
})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
255
|
+
const value = await response.json().catch((error) => {
|
|
256
|
+
if (response.status === 410) {
|
|
257
|
+
throw new BrowserImportError(
|
|
258
|
+
'incompatible',
|
|
259
|
+
'Could not verify the local receiver identity.'
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
throw error
|
|
263
|
+
})
|
|
264
|
+
if (response.status === 410) {
|
|
265
|
+
if (value?.code !== 'cancelled' || value.proof !== (await sign('cancelled')))
|
|
266
|
+
throw new BrowserImportError(
|
|
267
|
+
'incompatible',
|
|
268
|
+
'Could not verify the local receiver identity.'
|
|
269
|
+
)
|
|
270
|
+
cancelled = new BrowserImportError(
|
|
271
|
+
'cancelled',
|
|
272
|
+
'The transfer was cancelled in Open-Science. Prepare a new transfer to try again.'
|
|
237
273
|
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
throw cancelled
|
|
243
|
-
}
|
|
244
|
-
return response
|
|
245
|
-
}
|
|
274
|
+
throw cancelled
|
|
275
|
+
}
|
|
276
|
+
return { ok: response.ok, status: response.status, value }
|
|
277
|
+
})
|
|
246
278
|
try {
|
|
247
279
|
onPhase?.('waiting-for-receiver')
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
const budget =
|
|
280
|
+
// Count-only polling survives page suspension without consuming an overall deadline.
|
|
281
|
+
// Numeric/default budgets retain the legacy deadline across requests, bodies and gaps.
|
|
282
|
+
const budget = countOnly
|
|
283
|
+
? null
|
|
284
|
+
: Math.min(readyTimeoutMs, waitingForLaunch ? 120_000 : 300_000)
|
|
251
285
|
const attemptBudget = waitingForLaunch ? Math.min(attemptTimeoutMs, 2000) : attemptTimeoutMs
|
|
252
|
-
const deadline = Date.now() + budget
|
|
253
|
-
const
|
|
286
|
+
const deadline = budget === null ? null : Date.now() + budget
|
|
287
|
+
const poll = async (readySignal) => {
|
|
254
288
|
for (let attempt = 0; waitingForLaunch || attempt < readinessAttempts; attempt++) {
|
|
255
289
|
readySignal.throwIfAborted()
|
|
256
290
|
try {
|
|
@@ -259,7 +293,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
259
293
|
const response = await request(`/ready?challenge=${challenge}`, {
|
|
260
294
|
signal: attemptSignal
|
|
261
295
|
})
|
|
262
|
-
const value =
|
|
296
|
+
const value = response.value
|
|
263
297
|
if (
|
|
264
298
|
!value ||
|
|
265
299
|
typeof value !== 'object' ||
|
|
@@ -277,7 +311,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
277
311
|
`open-science-import-v1:${challenge}:${value.offset}:${value.accepted ? 1 : 0}`
|
|
278
312
|
)
|
|
279
313
|
attemptSignal.throwIfAborted()
|
|
280
|
-
if (Date.now() >= deadline)
|
|
314
|
+
if (deadline !== null && Date.now() >= deadline)
|
|
281
315
|
throw new BrowserImportError(
|
|
282
316
|
'timeout',
|
|
283
317
|
'Open-Science did not become ready in time.'
|
|
@@ -297,14 +331,16 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
297
331
|
'incompatible',
|
|
298
332
|
'Could not verify the local receiver identity.'
|
|
299
333
|
)
|
|
300
|
-
if (waitingForLaunch || attempt + 1 < readinessAttempts)
|
|
334
|
+
if (waitingForLaunch || attempt + 1 < readinessAttempts)
|
|
335
|
+
await wait(readinessIntervalMs, readySignal)
|
|
301
336
|
}
|
|
302
337
|
}
|
|
303
338
|
throw new BrowserImportError(
|
|
304
339
|
'readiness-exhausted',
|
|
305
340
|
'Open-Science did not become ready. Open the app, allow local network access, and retry.'
|
|
306
341
|
)
|
|
307
|
-
}
|
|
342
|
+
}
|
|
343
|
+
const ready = budget === null ? await poll(signal) : await bounded(signal, budget, poll)
|
|
308
344
|
verified = true
|
|
309
345
|
if (
|
|
310
346
|
!Number.isSafeInteger(ready.offset) ||
|
|
@@ -338,7 +374,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
338
374
|
}
|
|
339
375
|
})
|
|
340
376
|
if (!chunk.ok) throw new Error(`Local package transfer failed (${chunk.status}).`)
|
|
341
|
-
const result =
|
|
377
|
+
const result = chunk.value
|
|
342
378
|
if (
|
|
343
379
|
result.offset !== offset + bytes.byteLength ||
|
|
344
380
|
result.proof !== (await sign(`offset:${result.offset}`))
|
|
@@ -351,7 +387,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
351
387
|
method: 'POST',
|
|
352
388
|
headers: await authorize('POST', '/complete')
|
|
353
389
|
})
|
|
354
|
-
const receipt =
|
|
390
|
+
const receipt = complete.value
|
|
355
391
|
if (
|
|
356
392
|
complete.status !== 202 ||
|
|
357
393
|
receipt.received !== true ||
|
|
@@ -362,17 +398,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
362
398
|
received = true
|
|
363
399
|
return { received: true }
|
|
364
400
|
} catch (error) {
|
|
365
|
-
if (
|
|
366
|
-
await fetch(base, {
|
|
367
|
-
method: 'DELETE',
|
|
368
|
-
mode: 'cors',
|
|
369
|
-
credentials: 'omit',
|
|
370
|
-
redirect: 'error',
|
|
371
|
-
headers: await authorize('DELETE', ''),
|
|
372
|
-
signal: AbortSignal.timeout(2000),
|
|
373
|
-
targetAddressSpace: 'loopback'
|
|
374
|
-
}).catch(() => undefined)
|
|
375
|
-
}
|
|
401
|
+
if (signal?.aborted) await cancel()
|
|
376
402
|
signal?.throwIfAborted()
|
|
377
403
|
if (error.code === 'cancelled' || error.code === 'incompatible') throw error
|
|
378
404
|
if (verified)
|
package/cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ Commands:
|
|
|
40
40
|
stop Gracefully stop the backend
|
|
41
41
|
status Show backend status
|
|
42
42
|
url Print the authenticated web URL
|
|
43
|
+
mcp Serve research tools over MCP stdio (--config-root <path> or --profile <path>)
|
|
43
44
|
doctor --json Inspect headless readiness
|
|
44
45
|
runtime list List detected Agent runtimes
|
|
45
46
|
runtime install codex Prepare the app-managed Codex runtime
|
|
@@ -62,6 +63,8 @@ Commands:
|
|
|
62
63
|
session status <session-id>
|
|
63
64
|
session config show <session-id>
|
|
64
65
|
session config update <session-id> --revision <number> [session options]
|
|
66
|
+
session export <session-id> --project <id-or-name> --output <path>
|
|
67
|
+
session import <path> [--project <id-or-name> | --new-project <name>] --non-interactive
|
|
65
68
|
settings agent-routing show
|
|
66
69
|
settings agent-routing update [routing options]
|
|
67
70
|
plan show <session-id>
|
|
@@ -109,9 +112,11 @@ Options:
|
|
|
109
112
|
--wait Wait for the run to finish
|
|
110
113
|
--return-on-attention With --wait, return when the Plan needs approval
|
|
111
114
|
--timeout-ms <ms> Stop waiting after this many milliseconds
|
|
115
|
+
--output <path> Destination path (session export, artifacts download)
|
|
116
|
+
--new-project <name> Import into a new Project with this name (session import)
|
|
117
|
+
--non-interactive Import without an interactive preview confirmation (session import)
|
|
112
118
|
--cancel-on-timeout Cancel the server run when --timeout-ms expires
|
|
113
119
|
--jsonl With run --wait, stream one machine-readable event per line
|
|
114
|
-
--output <path> Artifact download destination
|
|
115
120
|
--credential-store <os|file> Settings credential storage (Linux headless; start only)
|
|
116
121
|
--no-open Do not open the browser after start
|
|
117
122
|
--no-sandbox Disable Chromium's process sandbox (security risk; start only)
|
|
@@ -160,7 +165,8 @@ const VALUE_OPTIONS = {
|
|
|
160
165
|
'--description': 'description',
|
|
161
166
|
'--agent-context': 'agentContext',
|
|
162
167
|
'--agent-context-file': 'agentContextFile',
|
|
163
|
-
'--output': 'output'
|
|
168
|
+
'--output': 'output',
|
|
169
|
+
'--new-project': 'newProject'
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
const TASK_COMMANDS = new Set([
|
|
@@ -198,6 +204,7 @@ const POSITIONAL_LIMITS = new Map([
|
|
|
198
204
|
['version', 0],
|
|
199
205
|
['help', 1],
|
|
200
206
|
['paths', 0],
|
|
207
|
+
['mcp', 0],
|
|
201
208
|
['doctor', 0],
|
|
202
209
|
['runtime list', 0],
|
|
203
210
|
['runtime install', 1],
|
|
@@ -228,6 +235,8 @@ const POSITIONAL_LIMITS = new Map([
|
|
|
228
235
|
['run cancel', 1],
|
|
229
236
|
['session status', 1],
|
|
230
237
|
['session config', 2],
|
|
238
|
+
['session export', 1],
|
|
239
|
+
['session import', 1],
|
|
231
240
|
['settings agent-routing', 1],
|
|
232
241
|
['plan show', 1],
|
|
233
242
|
['plan approve', 1],
|
|
@@ -275,9 +284,11 @@ export const parseCliArgs = (argv) => {
|
|
|
275
284
|
? ['--json', '--help', '-h']
|
|
276
285
|
: command === 'paths'
|
|
277
286
|
? ['--json', '--help', '-h', '--config-root', '--profile']
|
|
278
|
-
: command === '
|
|
279
|
-
? ['--help', '-h']
|
|
280
|
-
:
|
|
287
|
+
: command === 'mcp'
|
|
288
|
+
? ['--help', '-h', '--config-root', '--profile']
|
|
289
|
+
: command === 'help'
|
|
290
|
+
? ['--help', '-h']
|
|
291
|
+
: undefined
|
|
281
292
|
const subcommand =
|
|
282
293
|
(GROUP_COMMANDS.has(command) && args[0] && !args[0].startsWith('-')) ||
|
|
283
294
|
(command === 'run' && (args[0] === 'status' || args[0] === 'cancel'))
|
|
@@ -309,6 +320,7 @@ export const parseCliArgs = (argv) => {
|
|
|
309
320
|
else if (arg === '--wait') options.wait = true
|
|
310
321
|
else if (arg === '--return-on-attention') options.returnOnAttention = true
|
|
311
322
|
else if (arg === '--plan-first') options.planFirst = true
|
|
323
|
+
else if (arg === '--non-interactive') options.nonInteractive = true
|
|
312
324
|
else if (arg === '--provider-default-model') options.providerDefaultModel = true
|
|
313
325
|
else if (arg === '--clear-compute-hosts') options.clearComputeHosts = true
|
|
314
326
|
else if (arg === '--memory') {
|
|
@@ -464,8 +476,18 @@ export const parseCliArgs = (argv) => {
|
|
|
464
476
|
if (options.jsonl && (command !== 'run' || subcommand || !options.wait)) {
|
|
465
477
|
throw new CliUsageError('--jsonl requires run --wait.')
|
|
466
478
|
}
|
|
467
|
-
|
|
468
|
-
|
|
479
|
+
const isPackageTransfer = command === 'session' && ['export', 'import'].includes(subcommand)
|
|
480
|
+
if (
|
|
481
|
+
options.timeoutMs !== undefined &&
|
|
482
|
+
!isPackageTransfer &&
|
|
483
|
+
(command !== 'run' || subcommand || !options.wait)
|
|
484
|
+
) {
|
|
485
|
+
throw new CliUsageError('--timeout-ms requires run --wait or session export/import.')
|
|
486
|
+
}
|
|
487
|
+
if (command === 'session' && subcommand === 'import') {
|
|
488
|
+
if (!options.nonInteractive) throw new CliUsageError('--non-interactive is required.')
|
|
489
|
+
if (options.project && options.newProject)
|
|
490
|
+
throw new CliUsageError('Use only one of --project and --new-project.')
|
|
469
491
|
}
|
|
470
492
|
if (options.returnOnAttention && (command !== 'run' || subcommand || !options.wait)) {
|
|
471
493
|
throw new CliUsageError('--return-on-attention requires run --wait.')
|
|
@@ -1543,6 +1565,40 @@ export const runTaskCommand = async (parsed, dependencies = {}) => {
|
|
|
1543
1565
|
)
|
|
1544
1566
|
return
|
|
1545
1567
|
}
|
|
1568
|
+
if (command === 'session' && subcommand === 'export') {
|
|
1569
|
+
const sessionId = positionals[0]
|
|
1570
|
+
if (!sessionId) throw new CliUsageError('Session id is required.')
|
|
1571
|
+
if (!options.project) throw new CliUsageError('--project is required.')
|
|
1572
|
+
if (!options.output) throw new CliUsageError('--output is required.')
|
|
1573
|
+
const projectId = await resolveCliProjectId(client, options.project)
|
|
1574
|
+
outputValue(
|
|
1575
|
+
await client.exportSessionPackage(projectId, sessionId, resolve(options.output), {
|
|
1576
|
+
timeoutMs: options.timeoutMs
|
|
1577
|
+
}),
|
|
1578
|
+
options,
|
|
1579
|
+
deps
|
|
1580
|
+
)
|
|
1581
|
+
return
|
|
1582
|
+
}
|
|
1583
|
+
if (command === 'session' && subcommand === 'import') {
|
|
1584
|
+
const packagePath = positionals[0]
|
|
1585
|
+
if (!packagePath) throw new CliUsageError('Session package path is required.')
|
|
1586
|
+
const resolvedPath = resolve(packagePath)
|
|
1587
|
+
const target = {}
|
|
1588
|
+
if (options.project) target.projectId = await resolveCliProjectId(client, options.project)
|
|
1589
|
+
else if (options.newProject) target.projectName = options.newProject
|
|
1590
|
+
outputValue(
|
|
1591
|
+
await client.importSessionPackage(
|
|
1592
|
+
target,
|
|
1593
|
+
resolvedPath,
|
|
1594
|
+
{ nonInteractive: true },
|
|
1595
|
+
{ timeoutMs: options.timeoutMs }
|
|
1596
|
+
),
|
|
1597
|
+
options,
|
|
1598
|
+
deps
|
|
1599
|
+
)
|
|
1600
|
+
return
|
|
1601
|
+
}
|
|
1546
1602
|
if (command === 'settings' && subcommand === 'agent-routing') {
|
|
1547
1603
|
const action = positionals[0]
|
|
1548
1604
|
if (action === 'show') {
|
|
@@ -1826,7 +1882,11 @@ export const runCli = async (argv = process.argv.slice(2)) => {
|
|
|
1826
1882
|
const { version } = await readCliMetadata()
|
|
1827
1883
|
console.log(options.json ? JSON.stringify({ version }) : version)
|
|
1828
1884
|
} else if (command === 'paths') await pathsCommand(options)
|
|
1829
|
-
else if (command === '
|
|
1885
|
+
else if (command === 'mcp') {
|
|
1886
|
+
const { runResearchMcpServer } = await import('./mcp.mjs')
|
|
1887
|
+
const { version } = await readCliMetadata()
|
|
1888
|
+
await runResearchMcpServer({ configRoot: options.configRoot, version })
|
|
1889
|
+
} else if (command === 'init') await initCommand(options)
|
|
1830
1890
|
else if (command === 'start') await startCommand(options)
|
|
1831
1891
|
else if (command === 'stop') await stopCommand(options)
|
|
1832
1892
|
else if (command === 'status') await statusCommand(options)
|
package/index.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export type ConnectorsSnapshot = {
|
|
|
99
99
|
ncbi: NcbiCredentialsView
|
|
100
100
|
// Optional only for compatibility with an older main process during local development.
|
|
101
101
|
openAlex?: OpenAlexCredentialView
|
|
102
|
+
semanticScholar?: { hasApiKey: boolean }
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export type DeviceCredentialKind = 'api_key' | 'token' | 'oauth'
|
package/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const defaultSleep = (milliseconds) =>
|
|
|
6
6
|
new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds))
|
|
7
7
|
|
|
8
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
9
|
+
const PACKAGE_REQUEST_TIMEOUT_MS = 60 * 60 * 1_000
|
|
9
10
|
const DEFAULT_EVENT_IDLE_TIMEOUT_MS = 30_000
|
|
10
11
|
const EVENT_RECONNECT_DELAY_MS = 100
|
|
11
12
|
const MAX_BUFFERED_EVENTS = 1_024
|
|
@@ -308,6 +309,54 @@ export class OpenScienceClient {
|
|
|
308
309
|
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/config`, options)
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// Web RPC seam: authenticated loopback dispatch of one application command channel.
|
|
313
|
+
// The CLI reaches headless-only commands (Session package export/import) this way.
|
|
314
|
+
async callRpc(channel, args, options = {}) {
|
|
315
|
+
return withRequestTimeout(
|
|
316
|
+
{
|
|
317
|
+
defaultTimeoutMs: this.requestTimeoutMs,
|
|
318
|
+
signal: options.signal,
|
|
319
|
+
timeoutMs: options.timeoutMs
|
|
320
|
+
},
|
|
321
|
+
async (signal) => {
|
|
322
|
+
const response = await this.fetch(`${this.baseUrl}/rpc/${encodeURIComponent(channel)}`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
authorization: `Bearer ${this.token}`,
|
|
326
|
+
accept: 'application/json',
|
|
327
|
+
'content-type': 'application/json'
|
|
328
|
+
},
|
|
329
|
+
body: JSON.stringify({ protocolVersion: 1, args }),
|
|
330
|
+
signal
|
|
331
|
+
})
|
|
332
|
+
const payload = await response.json().catch(() => undefined)
|
|
333
|
+
if (!response.ok || payload?.ok !== true) {
|
|
334
|
+
throw new OpenScienceApiError(
|
|
335
|
+
payload?.error?.message ?? `Open-Science Web RPC request failed (${response.status}).`,
|
|
336
|
+
{ code: payload?.error?.code, status: response.status }
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
return payload.result
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
exportSessionPackage(projectId, sessionId, destinationPath, options) {
|
|
345
|
+
return this.callRpc('sessions:export-package', [{ projectId, sessionId }, destinationPath], {
|
|
346
|
+
...options,
|
|
347
|
+
timeoutMs: options?.timeoutMs ?? PACKAGE_REQUEST_TIMEOUT_MS
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
importSessionPackage(target, sourcePath, options, rpcOptions) {
|
|
352
|
+
if (options?.nonInteractive !== true)
|
|
353
|
+
throw new TypeError('Non-interactive Session import requires explicit confirmation.')
|
|
354
|
+
return this.callRpc('sessions:import-package', [target, sourcePath, { nonInteractive: true }], {
|
|
355
|
+
...rpcOptions,
|
|
356
|
+
timeoutMs: rpcOptions?.timeoutMs ?? PACKAGE_REQUEST_TIMEOUT_MS
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
311
360
|
updateSessionConfiguration(sessionId, request, options) {
|
|
312
361
|
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/config`, {
|
|
313
362
|
...options,
|
package/mcp.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { connectToOpenScience, OpenScienceApiError } from './index.mjs'
|
|
6
|
+
|
|
7
|
+
const id = (description) => z.string().trim().min(1).describe(description)
|
|
8
|
+
const sessionId = id('Session ID returned by list_sessions or start_run.')
|
|
9
|
+
const projectId = id('Project ID returned by list_projects or create_project.')
|
|
10
|
+
const runId = id('Run ID returned by start_run.')
|
|
11
|
+
const expectedRevision = z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.nonnegative()
|
|
15
|
+
.describe(
|
|
16
|
+
'Current revision returned by the corresponding read tool. Stale revisions are rejected.'
|
|
17
|
+
)
|
|
18
|
+
const agentConfiguration = z.strictObject({
|
|
19
|
+
providerId: id('Configured provider ID.').optional(),
|
|
20
|
+
model: id('Model identifier; null follows the provider default.').nullable().optional(),
|
|
21
|
+
reasoningEffort: z.enum(['default', 'low', 'medium', 'high', 'xhigh', 'max']).optional()
|
|
22
|
+
})
|
|
23
|
+
const sessionSettings = {
|
|
24
|
+
agentConfiguration: agentConfiguration.optional(),
|
|
25
|
+
permissionProfile: z
|
|
26
|
+
.enum(['ask', 'auto', 'full'])
|
|
27
|
+
.describe(
|
|
28
|
+
'Existing authorization policy. Omit to retain configured defaults; full grants broad execution authority.'
|
|
29
|
+
)
|
|
30
|
+
.optional(),
|
|
31
|
+
autoReviewEnabled: z.boolean().optional(),
|
|
32
|
+
memoryEnabled: z.boolean().optional(),
|
|
33
|
+
delegationPolicy: z.enum(['allow', 'deny']).optional()
|
|
34
|
+
}
|
|
35
|
+
const agentContext = z.string().trim().min(1).max(16000)
|
|
36
|
+
const read = {
|
|
37
|
+
readOnlyHint: true,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
idempotentHint: true,
|
|
40
|
+
openWorldHint: false
|
|
41
|
+
}
|
|
42
|
+
const write = {
|
|
43
|
+
readOnlyHint: false,
|
|
44
|
+
destructiveHint: true,
|
|
45
|
+
idempotentHint: false,
|
|
46
|
+
openWorldHint: true
|
|
47
|
+
}
|
|
48
|
+
const result = (value) => ({
|
|
49
|
+
content: [{ type: 'text', text: JSON.stringify({ result: value ?? null }) }],
|
|
50
|
+
structuredContent: { result: value ?? null }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Do not forward raw exception messages: HTTP failures may contain credentials or response bodies.
|
|
54
|
+
const toolError = (error) => {
|
|
55
|
+
const messages = {
|
|
56
|
+
daemon_unavailable:
|
|
57
|
+
'Open-Science is not running. Start it with "open-science start" for the selected profile, then retry.',
|
|
58
|
+
unsupported_capability:
|
|
59
|
+
'The service does not support this operation. Update the service before retrying.',
|
|
60
|
+
project_not_found: 'Project not found. Refresh list_projects.',
|
|
61
|
+
session_not_found: 'Session not found. Refresh list_sessions.',
|
|
62
|
+
run_not_found: 'Run not found. Check the ID returned by start_run.',
|
|
63
|
+
invalid_request: 'Invalid request. Check the tool arguments and service configuration.',
|
|
64
|
+
request_timeout:
|
|
65
|
+
'Request timed out. A write may already have succeeded; check its state before retrying.'
|
|
66
|
+
}
|
|
67
|
+
const code =
|
|
68
|
+
error instanceof OpenScienceApiError && Object.hasOwn(messages, error.code)
|
|
69
|
+
? error.code
|
|
70
|
+
: 'request_failed'
|
|
71
|
+
const status = error instanceof OpenScienceApiError ? error.status : undefined
|
|
72
|
+
const message =
|
|
73
|
+
status === 401 || status === 403
|
|
74
|
+
? 'Access denied. Check the selected profile and its service credentials.'
|
|
75
|
+
: status === 409
|
|
76
|
+
? 'The operation conflicts with current state. Read the latest state and revision before retrying.'
|
|
77
|
+
: (messages[code] ??
|
|
78
|
+
'The operation failed. Check service readiness and request arguments. A write may already have succeeded; inspect its state before retrying.')
|
|
79
|
+
return {
|
|
80
|
+
isError: true,
|
|
81
|
+
content: [{ type: 'text', text: JSON.stringify({ error: { code, message } }) }]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const createResearchMcpServer = ({
|
|
86
|
+
configRoot,
|
|
87
|
+
version,
|
|
88
|
+
connect = connectToOpenScience
|
|
89
|
+
}) => {
|
|
90
|
+
const server = new McpServer({ name: 'open-science', version })
|
|
91
|
+
const lifetime = new AbortController()
|
|
92
|
+
const register = (name, description, shape, annotations, invoke) => {
|
|
93
|
+
server.registerTool(
|
|
94
|
+
name,
|
|
95
|
+
{
|
|
96
|
+
description,
|
|
97
|
+
inputSchema: z.strictObject(shape),
|
|
98
|
+
outputSchema: z.object({ result: z.unknown() }),
|
|
99
|
+
annotations
|
|
100
|
+
},
|
|
101
|
+
async (args, extra) => {
|
|
102
|
+
const signal = AbortSignal.any([lifetime.signal, extra.signal])
|
|
103
|
+
try {
|
|
104
|
+
signal.throwIfAborted()
|
|
105
|
+
// Rediscover per call so service restarts/token rotation recover without retrying writes.
|
|
106
|
+
const client = await connect({ configRoot, signal })
|
|
107
|
+
signal.throwIfAborted()
|
|
108
|
+
return result(await invoke(client, args, { signal }))
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return toolError(error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
register(
|
|
116
|
+
'doctor',
|
|
117
|
+
'Check whether the running Open-Science service is ready to execute research tasks. Does not start or configure the service.',
|
|
118
|
+
{},
|
|
119
|
+
read,
|
|
120
|
+
(client, _args, options) => client.doctor(options)
|
|
121
|
+
)
|
|
122
|
+
register(
|
|
123
|
+
'list_runtimes',
|
|
124
|
+
'List detected Agent runtimes and readiness. Does not install runtimes or change the selected framework.',
|
|
125
|
+
{},
|
|
126
|
+
read,
|
|
127
|
+
(client, _args, options) => client.listRuntimes(options)
|
|
128
|
+
)
|
|
129
|
+
register(
|
|
130
|
+
'list_projects',
|
|
131
|
+
'List projects and their IDs before choosing a research destination.',
|
|
132
|
+
{},
|
|
133
|
+
read,
|
|
134
|
+
(client, _args, options) => client.listProjects(options)
|
|
135
|
+
)
|
|
136
|
+
register(
|
|
137
|
+
'create_project',
|
|
138
|
+
'Create a persistent research project. Agent Context applies to future work in this project.',
|
|
139
|
+
{
|
|
140
|
+
name: id('Project name.'),
|
|
141
|
+
description: z.string().optional(),
|
|
142
|
+
agentContext: agentContext.optional()
|
|
143
|
+
},
|
|
144
|
+
write,
|
|
145
|
+
(client, args, options) => client.createProject(args, options)
|
|
146
|
+
)
|
|
147
|
+
register(
|
|
148
|
+
'update_project',
|
|
149
|
+
'Update a persistent project. Only supplied fields change; null clears Agent Context.',
|
|
150
|
+
{
|
|
151
|
+
projectId,
|
|
152
|
+
name: id('New project name.').optional(),
|
|
153
|
+
description: z.string().optional(),
|
|
154
|
+
agentContext: agentContext.nullable().optional()
|
|
155
|
+
},
|
|
156
|
+
write,
|
|
157
|
+
(client, { projectId, ...patch }, options) => client.updateProject(projectId, patch, options)
|
|
158
|
+
)
|
|
159
|
+
register(
|
|
160
|
+
'list_sessions',
|
|
161
|
+
'List sessions, optionally within one project. Use the returned ID to inspect or continue a session.',
|
|
162
|
+
{
|
|
163
|
+
projectId: projectId.optional()
|
|
164
|
+
},
|
|
165
|
+
read,
|
|
166
|
+
(client, args, options) => client.listSessions(args.projectId, options)
|
|
167
|
+
)
|
|
168
|
+
register(
|
|
169
|
+
'get_session',
|
|
170
|
+
'Read session information and current status. This is not a full conversation transcript.',
|
|
171
|
+
{ sessionId },
|
|
172
|
+
read,
|
|
173
|
+
(client, args, options) => client.getSession(args.sessionId, options)
|
|
174
|
+
)
|
|
175
|
+
register(
|
|
176
|
+
'get_session_configuration',
|
|
177
|
+
'Read current session configuration and revision before changing it.',
|
|
178
|
+
{ sessionId },
|
|
179
|
+
read,
|
|
180
|
+
(client, args, options) => client.getSessionConfiguration(args.sessionId, options)
|
|
181
|
+
)
|
|
182
|
+
register(
|
|
183
|
+
'update_session_configuration',
|
|
184
|
+
'Update persistent session settings using the latest revision. May change model costs and execution permissions; only supplied fields change.',
|
|
185
|
+
{
|
|
186
|
+
sessionId,
|
|
187
|
+
expectedRevision,
|
|
188
|
+
...sessionSettings,
|
|
189
|
+
computeHosts: z
|
|
190
|
+
.strictObject({
|
|
191
|
+
enabled: z.array(id('Enabled Compute Host ID.')),
|
|
192
|
+
selected: z.array(id('Selected Compute Host ID.'))
|
|
193
|
+
})
|
|
194
|
+
.optional()
|
|
195
|
+
},
|
|
196
|
+
write,
|
|
197
|
+
(client, { sessionId, ...patch }, options) =>
|
|
198
|
+
client.updateSessionConfiguration(sessionId, patch, options)
|
|
199
|
+
)
|
|
200
|
+
register(
|
|
201
|
+
'start_run',
|
|
202
|
+
'Submit research in a project and immediately return a run ID. Omit sessionId to create a session, or provide it to continue that session through its existing active branch. May write files/data and incur model charges. Poll get_run; disconnecting does not cancel the run. Uses configured framework and permissions, without automatically approving human interactions.',
|
|
203
|
+
{
|
|
204
|
+
projectId,
|
|
205
|
+
prompt: id('Research instructions.'),
|
|
206
|
+
sessionId: sessionId.optional(),
|
|
207
|
+
cwd: id('Working directory on the Open-Science host.').optional(),
|
|
208
|
+
...sessionSettings,
|
|
209
|
+
skillIds: z.array(id('Skill ID.')).optional(),
|
|
210
|
+
specialist: id('Specialist ID or exact name for a new session.').optional(),
|
|
211
|
+
turnIntent: z
|
|
212
|
+
.literal('plan-first')
|
|
213
|
+
.describe('Require an approved plan before execution.')
|
|
214
|
+
.optional(),
|
|
215
|
+
permissionPrompts: z
|
|
216
|
+
.literal('none')
|
|
217
|
+
.describe(
|
|
218
|
+
'Deny unresolved human interactions instead of waiting; does not expand permissions. Incompatible with plan-first.'
|
|
219
|
+
)
|
|
220
|
+
.optional(),
|
|
221
|
+
computeHostIds: z.array(id('Selected Compute Host ID.')).optional(),
|
|
222
|
+
enabledComputeHostIds: z.array(id('Enabled Compute Host ID for a new session.')).optional()
|
|
223
|
+
},
|
|
224
|
+
write,
|
|
225
|
+
(client, { projectId, ...request }, options) => {
|
|
226
|
+
if (request.turnIntent === 'plan-first' && request.permissionPrompts === 'none') {
|
|
227
|
+
throw new OpenScienceApiError('Incompatible permission and planning options.', {
|
|
228
|
+
code: 'invalid_request'
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
return client.startRun({ project: projectId, ...request }, options)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
register(
|
|
235
|
+
'get_run',
|
|
236
|
+
'Read authoritative run status, progress, result and attention state. Poll this after start_run; a waiting run may require human action in Open-Science.',
|
|
237
|
+
{ runId },
|
|
238
|
+
read,
|
|
239
|
+
(client, args, options) => client.getRun(args.runId, options)
|
|
240
|
+
)
|
|
241
|
+
register(
|
|
242
|
+
'cancel_run',
|
|
243
|
+
'Explicitly cancel a research run and wait for service finalization. This does not undo files, data or costs already produced.',
|
|
244
|
+
{ runId },
|
|
245
|
+
write,
|
|
246
|
+
(client, args, options) => client.cancelRun(args.runId, options)
|
|
247
|
+
)
|
|
248
|
+
register(
|
|
249
|
+
'get_plan',
|
|
250
|
+
'Read the current session plan, artifact version and revision, or null when there is no plan.',
|
|
251
|
+
{ sessionId },
|
|
252
|
+
read,
|
|
253
|
+
(client, args, options) => client.getSessionPlan(args.sessionId, options)
|
|
254
|
+
)
|
|
255
|
+
register(
|
|
256
|
+
'respond_plan',
|
|
257
|
+
'Explicitly approve or reject the reviewed plan version, or request revision with feedback. Approval may resume execution and incur costs. Never approve without reviewing get_plan.',
|
|
258
|
+
{
|
|
259
|
+
sessionId,
|
|
260
|
+
response: z.union([
|
|
261
|
+
z.strictObject({
|
|
262
|
+
decision: z.enum(['approved', 'rejected']),
|
|
263
|
+
artifactVersionId: id('Plan artifact version returned by get_plan.'),
|
|
264
|
+
expectedRevision
|
|
265
|
+
}),
|
|
266
|
+
z.strictObject({ feedback: id('Requested changes to the plan.') })
|
|
267
|
+
])
|
|
268
|
+
},
|
|
269
|
+
write,
|
|
270
|
+
(client, args, options) => client.respondSessionPlan(args.sessionId, args.response, options)
|
|
271
|
+
)
|
|
272
|
+
register(
|
|
273
|
+
'list_artifacts',
|
|
274
|
+
'List generated artifact metadata for a session. Does not download files or write local paths.',
|
|
275
|
+
{ sessionId },
|
|
276
|
+
read,
|
|
277
|
+
(client, args, options) => client.listArtifacts(args.sessionId, options)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const onclose = server.server.onclose
|
|
281
|
+
server.server.onclose = () => {
|
|
282
|
+
lifetime.abort()
|
|
283
|
+
onclose?.()
|
|
284
|
+
}
|
|
285
|
+
return server
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const runResearchMcpServer = async (options) => {
|
|
289
|
+
const server = createResearchMcpServer(options)
|
|
290
|
+
const transport = new StdioServerTransport()
|
|
291
|
+
let closing
|
|
292
|
+
const close = () => {
|
|
293
|
+
closing ??= server.close().finally(() => {
|
|
294
|
+
process.stdin.off('end', close)
|
|
295
|
+
process.off('SIGINT', close)
|
|
296
|
+
process.off('SIGTERM', close)
|
|
297
|
+
})
|
|
298
|
+
return closing
|
|
299
|
+
}
|
|
300
|
+
process.stdin.once('end', close)
|
|
301
|
+
process.once('SIGINT', close)
|
|
302
|
+
process.once('SIGTERM', close)
|
|
303
|
+
try {
|
|
304
|
+
await server.connect(transport)
|
|
305
|
+
if (process.stdin.readableEnded) await close()
|
|
306
|
+
} catch (error) {
|
|
307
|
+
await close()
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipoch/open-science",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Node.js SDK and CLI for the Open-Science local daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -37,11 +37,15 @@
|
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
42
|
+
"zod": "^4.6.5"
|
|
43
|
+
},
|
|
40
44
|
"optionalDependencies": {
|
|
41
|
-
"@aipoch/open-science-darwin-arm64": "1.0.
|
|
42
|
-
"@aipoch/open-science-darwin-x64": "1.0.
|
|
43
|
-
"@aipoch/open-science-linux-x64-gnu": "1.0.
|
|
44
|
-
"@aipoch/open-science-linux-arm64-gnu": "1.0.
|
|
45
|
-
"@aipoch/open-science-win32-x64": "1.0.
|
|
45
|
+
"@aipoch/open-science-darwin-arm64": "1.0.4",
|
|
46
|
+
"@aipoch/open-science-darwin-x64": "1.0.4",
|
|
47
|
+
"@aipoch/open-science-linux-x64-gnu": "1.0.4",
|
|
48
|
+
"@aipoch/open-science-linux-arm64-gnu": "1.0.4",
|
|
49
|
+
"@aipoch/open-science-win32-x64": "1.0.4"
|
|
46
50
|
}
|
|
47
51
|
}
|