@wenathlan/devthink 2.0.14 → 2.0.19
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 +60 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/mcp.ts
DELETED
|
@@ -1,1835 +0,0 @@
|
|
|
1
|
-
import { protocolmajorversion, protocolversion } from "./types.js";
|
|
2
|
-
import { negotiateprotocol, protocolmajorof } from "./apifreeze.js";
|
|
3
|
-
import type {
|
|
4
|
-
agentplan,
|
|
5
|
-
agentsession,
|
|
6
|
-
callcontext,
|
|
7
|
-
capabilityset,
|
|
8
|
-
clientrecord,
|
|
9
|
-
jsonrpcframe,
|
|
10
|
-
mcpserverconfig,
|
|
11
|
-
methodentry,
|
|
12
|
-
rpcerror,
|
|
13
|
-
rpcerrorcode,
|
|
14
|
-
stdiobridge,
|
|
15
|
-
toolcatalog,
|
|
16
|
-
toolcallrecord,
|
|
17
|
-
toolnamespace,
|
|
18
|
-
toolresult,
|
|
19
|
-
toolstep,
|
|
20
|
-
transportkind,
|
|
21
|
-
allowlistentry,
|
|
22
|
-
approvalrequest,
|
|
23
|
-
auditevent,
|
|
24
|
-
batchcall,
|
|
25
|
-
batchoutcome,
|
|
26
|
-
callratelimit,
|
|
27
|
-
clientbinding,
|
|
28
|
-
idempotencyrecord,
|
|
29
|
-
progressnotice,
|
|
30
|
-
prompttemplate,
|
|
31
|
-
resourcewatch,
|
|
32
|
-
samplingrequest,
|
|
33
|
-
servehealth,
|
|
34
|
-
servedresource,
|
|
35
|
-
servetransport,
|
|
36
|
-
streamchunk,
|
|
37
|
-
structurederror,
|
|
38
|
-
toolmock,
|
|
39
|
-
} from "./types.js";
|
|
40
|
-
/* ── The mcp entry section the family kept before the 1.1.88 merge. ── */
|
|
41
|
-
/** The mcp server library entry of the 1.1.84 family: the full model context protocol server surface of devthink as one tree shakable entry — the serve mode with its frame router, session bindings, approval gates and dispatch composition, the json rpc message codec with its structured retry hints, the stdio and http transports with their injected seams, the served resources with their change subscriptions, the prompt exposure of the template library and the walkthroughs, and the tool catalog, call runtime, client auth, http stream and server contract families the serve mode composes; the 1.1.85 native host bridge family joins the entry so the nativehost and wsbridge surfaces — the native call events the subscribed mcp clients stream and the servercontract envelope translation the wsbridge frames reuse — ride the same tree shakable server surface. */
|
|
42
|
-
export * from "./serve.js";
|
|
43
|
-
export * from "./serve.js";
|
|
44
|
-
export * from "./serve.js";
|
|
45
|
-
export * from "./http.js";
|
|
46
|
-
export * from "./http.js";
|
|
47
|
-
export * from "./tools.js";
|
|
48
|
-
export * from "./agent.js";
|
|
49
|
-
export * from "./tools.js";
|
|
50
|
-
export * from "./auth.js";
|
|
51
|
-
export * from "./gates.js";
|
|
52
|
-
export * from "./http.js";
|
|
53
|
-
export * from "./bridge.js";
|
|
54
|
-
export * from "./bridge.js";
|
|
55
|
-
export * from "./bridge.js";
|
|
56
|
-
export * from "./policy.js";
|
|
57
|
-
|
|
58
|
-
/* ── Merged from mcpmode.ts: the 1.1.88 consolidation interns the correlated mcpmode logic here, so no variation of the same file lives beside another. ── */
|
|
59
|
-
import { notifyprogress, requestsampling, streamchunkof, canceltool, callprompt, listprompts } from "./agent.js";
|
|
60
|
-
import { requireapproval } from "./gates.js";
|
|
61
|
-
import { checkallowlist } from "./auth.js";
|
|
62
|
-
import { notificationframe } from "./serve.js";
|
|
63
|
-
import { randomid } from "./memory.js";
|
|
64
|
-
import {
|
|
65
|
-
degradationgate,
|
|
66
|
-
mcpmodegate,
|
|
67
|
-
promptexposuregate,
|
|
68
|
-
resourceexposuregate,
|
|
69
|
-
tooldispatchgate,
|
|
70
|
-
unknownfieldsgate,
|
|
71
|
-
} from "./policy.js";
|
|
72
|
-
import { listmcpprompts, rendermcpprompt } from "./serve.js";
|
|
73
|
-
import { publishchange, readresource, resourceof, resourceslist, servedresources, subscriberesource } from "./serve.js";
|
|
74
|
-
import {
|
|
75
|
-
composeenvelope,
|
|
76
|
-
eventpostbody,
|
|
77
|
-
frameof,
|
|
78
|
-
progresseventpayload,
|
|
79
|
-
servercontractversion,
|
|
80
|
-
stableopid,
|
|
81
|
-
} from "./bridge.js";
|
|
82
|
-
import { packageversion } from "./version.js";
|
|
83
|
-
import { alltools, buildtoolcatalog, namespaceof, resolvetool, toolnamespaces } from "./tools.js";
|
|
84
|
-
import {
|
|
85
|
-
applymock,
|
|
86
|
-
applyratelimit,
|
|
87
|
-
begincall,
|
|
88
|
-
checkidempotency,
|
|
89
|
-
dryruntool,
|
|
90
|
-
endcall,
|
|
91
|
-
recordidempotency,
|
|
92
|
-
runbatch,
|
|
93
|
-
} from "./tools.js";
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Mcp server mode of the 1.1.84 family.
|
|
97
|
-
* Every serve mode concern lives in this file: the serve entry that runs devthink as a model context protocol server over the stdio and http transports, the initialization with the capability negotiation and the server metadata that reports the mcp version alongside the server contract version, the client metadata of the initialize handshake that records the client name and version for the audit trail, the session bindings that pair one client with exactly one extension session so concurrent clients hold isolated sessions, the read only degradation when no origin grant covers the session, the exposed tool filtering by scopes and degradation so the serve never lists a tool the current grants do not cover, the health resource with its version and uptime, the shutdown drain that waits for the in flight calls before the exit, the plan progress events that reuse the server contract envelopes, the sampling frames that route generation requests back into the client model, the stream chunk and progress notifications of long calls, the mock, dry run, idempotency and batch dispatch composition, the allowlist and rate limit admission, the approval gated sensitive calls that block until the human answers, the audit entries that name the caller, the tool and the outcome, and the serve frame router with its extended method table.
|
|
98
|
-
* The mode stays pure and shares the engine: the policy gates, the kind catalog, the memory store and the progress store ride through the same modules the extension surfaces use, execution flows through the injected execute seam so tests run on plain fixtures, and no port, window, ceiling, endpoint, provider or key is ever hardcoded.
|
|
99
|
-
*/
|
|
100
|
-
|
|
101
|
-
/** The serve method table of the mcp server mode: the protocol family methods beside the resource, prompt, batch and health surfaces the serve mode adds. */
|
|
102
|
-
export function servemethods(): methodentry[] {
|
|
103
|
-
return [
|
|
104
|
-
{
|
|
105
|
-
method: "initialize",
|
|
106
|
-
handler: "initialize",
|
|
107
|
-
description:
|
|
108
|
-
"Completes the mcp handshake, records the client metadata and returns the server info with the server contract version.",
|
|
109
|
-
},
|
|
110
|
-
{ method: "ping", handler: "ping", description: "Answers keepalive frames with pong." },
|
|
111
|
-
{
|
|
112
|
-
method: "tools/list",
|
|
113
|
-
handler: "listtools",
|
|
114
|
-
description:
|
|
115
|
-
"Returns every exposed tool the current grants cover with its version, json schema inputs and consent metadata.",
|
|
116
|
-
},
|
|
117
|
-
{ method: "negotiate", handler: "negotiate", description: "Exchanges capability sets with the client." },
|
|
118
|
-
{
|
|
119
|
-
method: "tools/call",
|
|
120
|
-
handler: "dispatch",
|
|
121
|
-
description:
|
|
122
|
-
"Invokes one tool behind the consent gates with the mock, dry run, idempotency and approval composition.",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
method: "prompts/list",
|
|
126
|
-
handler: "listprompts",
|
|
127
|
-
description: "Lists the walkthrough prompts, the user authored template library and the protocol prompt tools.",
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
method: "prompts/call",
|
|
131
|
-
handler: "callprompt",
|
|
132
|
-
description: "Renders one served prompt with its declared arguments.",
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
method: "resources/list",
|
|
136
|
-
handler: "listresources",
|
|
137
|
-
description: "Lists every served resource with its uri, description and mimetype.",
|
|
138
|
-
},
|
|
139
|
-
{ method: "resources/read", handler: "readresource", description: "Reads one served resource into its wire text." },
|
|
140
|
-
{
|
|
141
|
-
method: "resources/subscribe",
|
|
142
|
-
handler: "subscriberesource",
|
|
143
|
-
description: "Opens one resource subscription with its change notifications.",
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
method: "resources/unsubscribe",
|
|
147
|
-
handler: "unsubscriberesource",
|
|
148
|
-
description: "Closes one resource subscription.",
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
method: "calls/cancel",
|
|
152
|
-
handler: "cancel",
|
|
153
|
-
description: "Aborts one in flight tool call and preserves its partial result.",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
method: "calls/batch",
|
|
157
|
-
handler: "batch",
|
|
158
|
-
description: "Runs an ordered batch of tool calls in one request with the stop on first error flag.",
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
method: "health",
|
|
162
|
-
handler: "health",
|
|
163
|
-
description: "Returns the health resource with the version and the uptime.",
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** The server metadata of the serve mode: the mcp identity, the protocol version, the server contract version reported alongside it, the tool, resource and prompt counts and the instructions that state the consent model in plain language. */
|
|
169
|
-
export function servemetadata(input: { config: mcpserverconfig; catalog: toolcatalog; templates: prompttemplate[] }): {
|
|
170
|
-
serverinfo: capabilityset & { servercontractversion: number; resourcecount: number; promptcount: number };
|
|
171
|
-
protocolversion: string;
|
|
172
|
-
servercontractversion: number;
|
|
173
|
-
instructions: string;
|
|
174
|
-
} {
|
|
175
|
-
const capabilities = servercapabilities({ config: input.config, catalog: input.catalog });
|
|
176
|
-
return {
|
|
177
|
-
serverinfo: {
|
|
178
|
-
...capabilities,
|
|
179
|
-
servercontractversion,
|
|
180
|
-
resourcecount: servedresources().length,
|
|
181
|
-
promptcount: listmcpprompts(input.templates).length,
|
|
182
|
-
},
|
|
183
|
-
protocolversion,
|
|
184
|
-
servercontractversion,
|
|
185
|
-
instructions: `Devthink serves its browser tools, resources and prompts behind the human review gates: read only tools run once a session is approved, every tool with side effects executes exactly the approved plan step it names and blocks until the human approval gate answers, the served resources carry no page content the user granted none of, and every call lands in the audit trail with its caller, tool and outcome. The server contract version ${servercontractversion} rides beside the mcp version ${protocolversion} on every event envelope. No endpoint, provider or key is hardcoded; the user pairs every client.`,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Reads the client metadata of one initialize handshake: the client declares its name and version so the audit trail records exactly which build called. */
|
|
190
|
-
export function clientmetadataof(params: Record<string, unknown> | undefined): { name?: string; version?: string } {
|
|
191
|
-
const info = params?.clientinfo ?? params?.client;
|
|
192
|
-
if (info === undefined || info === null || typeof info !== "object" || Array.isArray(info)) return {};
|
|
193
|
-
const candidate = info as Record<string, unknown>;
|
|
194
|
-
const name = typeof candidate.name === "string" && candidate.name.trim() !== "" ? candidate.name.trim() : undefined;
|
|
195
|
-
const version =
|
|
196
|
-
typeof candidate.version === "string" && candidate.version.trim() !== "" ? candidate.version.trim() : undefined;
|
|
197
|
-
return { ...(name !== undefined ? { name } : {}), ...(version !== undefined ? { version } : {}) };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Stamps one client record with the declared client metadata: the name and version ride the record so every audit entry of its calls names the caller build. */
|
|
201
|
-
export function recordclientmetadata(
|
|
202
|
-
client: clientrecord,
|
|
203
|
-
metadata: { name?: string; version?: string },
|
|
204
|
-
): clientrecord {
|
|
205
|
-
return {
|
|
206
|
-
...client,
|
|
207
|
-
...(metadata.name !== undefined ? { name: metadata.name } : {}),
|
|
208
|
-
...(metadata.version !== undefined ? { clientversion: metadata.version } : {}),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Completes the serve initialize handshake: the server metadata with the negotiated capability set and the client metadata recorded on the client record the caller persists. */
|
|
213
|
-
export function serveinitialize(input: {
|
|
214
|
-
params?: Record<string, unknown>;
|
|
215
|
-
config: mcpserverconfig;
|
|
216
|
-
catalog: toolcatalog;
|
|
217
|
-
templates: prompttemplate[];
|
|
218
|
-
}): { handshake: ReturnType<typeof servemetadata>; client: (client: clientrecord) => clientrecord } {
|
|
219
|
-
return {
|
|
220
|
-
handshake: servemetadata({ config: input.config, catalog: input.catalog, templates: input.templates }),
|
|
221
|
-
client: (client: clientrecord) => recordclientmetadata(client, clientmetadataof(input.params)),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Builds the transport endpoints of the serve mode: the stdio transport rides the process pipes while the http transport rides its own localhost bind with its own port, and both run at the same time on separate channels when the config allows both. */
|
|
226
|
-
export function transportendpoints(input: { config: mcpserverconfig; now: number }): servetransport[] {
|
|
227
|
-
const transports: servetransport[] = [];
|
|
228
|
-
if (input.config.transports.includes("stdio"))
|
|
229
|
-
transports.push({ kind: "stdio", endpoint: "stdio://devthink", startedat: input.now });
|
|
230
|
-
if (input.config.transports.includes("http")) {
|
|
231
|
-
const bind =
|
|
232
|
-
input.config.bind !== undefined && input.config.bind.trim() !== "" ? input.config.bind.trim() : "127.0.0.1";
|
|
233
|
-
const path =
|
|
234
|
-
input.config.httpstream?.endpoint !== undefined && input.config.httpstream.endpoint.trim() !== ""
|
|
235
|
-
? input.config.httpstream.endpoint.trim()
|
|
236
|
-
: "/mcp";
|
|
237
|
-
transports.push({ kind: "http", endpoint: `http://${bind}:${input.config.port}${path}`, startedat: input.now });
|
|
238
|
-
}
|
|
239
|
-
return transports;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** Starts the serve mode: the mcpmode gate must pass, the transports report their endpoints and the degradation gate decides the read only flag from the session grants; a refused gate reports its reason instead of a state. */
|
|
243
|
-
export function startserve(input: { config: mcpserverconfig; now: number; grants?: string[]; origin?: string }): {
|
|
244
|
-
state?: { state: "running"; transports: servetransport[]; startedat: number; degraded: boolean };
|
|
245
|
-
reason?: string;
|
|
246
|
-
} {
|
|
247
|
-
const gate = mcpmodegate(input.config);
|
|
248
|
-
if (!gate.allowed) return { reason: gate.reason ?? "The serve mode failed its gate." };
|
|
249
|
-
const degradation = degradationgate({
|
|
250
|
-
...(input.grants !== undefined ? { grants: input.grants } : {}),
|
|
251
|
-
...(input.origin !== undefined ? { origin: input.origin } : {}),
|
|
252
|
-
});
|
|
253
|
-
return {
|
|
254
|
-
state: {
|
|
255
|
-
state: "running",
|
|
256
|
-
transports: transportendpoints({ config: input.config, now: input.now }),
|
|
257
|
-
startedat: input.now,
|
|
258
|
-
degraded: !degradation.allowed,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** The shutdown drain of the serve mode: the drain waits for the in flight calls before the exit — a stopped answer needs an empty in flight set or a drain window the user configured that elapsed. */
|
|
264
|
-
export function shutdowndrain(input: { inflight: callcontext[]; drainstart?: number; now: number; window?: number }): {
|
|
265
|
-
phase: "draining" | "stopped";
|
|
266
|
-
waiting: number;
|
|
267
|
-
} {
|
|
268
|
-
const waiting = input.inflight.filter((context) => context.state === "inflight").length;
|
|
269
|
-
if (waiting === 0) return { phase: "stopped", waiting: 0 };
|
|
270
|
-
if (input.window !== undefined && input.drainstart !== undefined && input.now - input.drainstart >= input.window)
|
|
271
|
-
return { phase: "stopped", waiting };
|
|
272
|
-
return { phase: "draining", waiting };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Builds the health resource of the serve mode: the extension version, the protocol version, the server contract version, the uptime and the serve state — no payload, origin or client identity rides the line. */
|
|
276
|
-
export function healthof(input: { startedat: number; now: number; state: string }): servehealth {
|
|
277
|
-
return {
|
|
278
|
-
version: packageversion,
|
|
279
|
-
protocolversion,
|
|
280
|
-
servercontractversion,
|
|
281
|
-
uptime: Math.max(0, input.now - input.startedat),
|
|
282
|
-
state: input.state,
|
|
283
|
-
at: input.now,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Binds one client to exactly one extension session: concurrent clients hold isolated sessions, the token id rides the binding when the pairing issued one, and the degradation gate decides the read only flag from the grants. */
|
|
288
|
-
export function bindclientsession(input: {
|
|
289
|
-
clientid: string;
|
|
290
|
-
sessionid: string;
|
|
291
|
-
tokenid?: string;
|
|
292
|
-
grants?: string[];
|
|
293
|
-
origin?: string;
|
|
294
|
-
now: number;
|
|
295
|
-
}): clientbinding {
|
|
296
|
-
const degradation = degradationgate({
|
|
297
|
-
...(input.grants !== undefined ? { grants: input.grants } : {}),
|
|
298
|
-
...(input.origin !== undefined ? { origin: input.origin } : {}),
|
|
299
|
-
});
|
|
300
|
-
return {
|
|
301
|
-
clientid: input.clientid,
|
|
302
|
-
sessionid: input.sessionid,
|
|
303
|
-
...(input.tokenid !== undefined ? { tokenid: input.tokenid } : {}),
|
|
304
|
-
readonly: !degradation.allowed,
|
|
305
|
-
boundat: input.now,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Releases one client binding; the record stays for the audit trail with its release time. */
|
|
310
|
-
export function releasebinding(bindings: clientbinding[], clientid: string, now: number): clientbinding[] {
|
|
311
|
-
return bindings.map((binding) =>
|
|
312
|
-
binding.clientid === clientid && binding.releasedat === undefined ? { ...binding, releasedat: now } : binding,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** True when every live binding names a distinct extension session so concurrent clients hold isolated sessions. */
|
|
317
|
-
export function isolatedsessions(bindings: clientbinding[]): boolean {
|
|
318
|
-
const live = bindings.filter((binding) => binding.releasedat === undefined);
|
|
319
|
-
return new Set(live.map((binding) => binding.sessionid)).size === live.length;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Filters the catalog to the tools the current grants cover: a degraded binding exposes the read only tools alone while the scopes narrow the namespaces the session token grants. */
|
|
323
|
-
export function exposedtools(
|
|
324
|
-
catalog: toolcatalog,
|
|
325
|
-
options: { readonly?: boolean; scopes?: toolnamespace[] } = {},
|
|
326
|
-
): toolcatalog {
|
|
327
|
-
const exposed = alltools(catalog).filter(
|
|
328
|
-
(tool) =>
|
|
329
|
-
(options.readonly !== true || tool.risk === "read") &&
|
|
330
|
-
(options.scopes === undefined || options.scopes.includes(namespaceof(tool.name) as toolnamespace)),
|
|
331
|
-
);
|
|
332
|
-
return {
|
|
333
|
-
version: catalog.version,
|
|
334
|
-
domains: catalog.domains
|
|
335
|
-
.map((domain) => ({
|
|
336
|
-
...domain,
|
|
337
|
-
tools: domain.tools.filter((tool) => exposed.some((entry) => entry.name === tool.name)),
|
|
338
|
-
}))
|
|
339
|
-
.filter((domain) => domain.tools.length > 0),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Builds one plan progress event of the serve mode: the progress payload rides the server contract envelope of the eventpost operation so subscribed clients read the same envelope shape the bridge family streams, and the wire form is the newline terminated frame text the stream channel emits. */
|
|
344
|
-
export function planprogressevent(input: {
|
|
345
|
-
stepid: string;
|
|
346
|
-
status: string;
|
|
347
|
-
sessionid?: string;
|
|
348
|
-
sequence: number;
|
|
349
|
-
at: number;
|
|
350
|
-
}): { envelope: ReturnType<typeof composeenvelope>; wire: string } {
|
|
351
|
-
const envelope = composeenvelope({
|
|
352
|
-
op: "eventpost",
|
|
353
|
-
opid: stableopid(`mcpmode-progress-${input.stepid}`, input.sequence),
|
|
354
|
-
at: input.at,
|
|
355
|
-
version: servercontractversion,
|
|
356
|
-
...(input.sessionid !== undefined ? { sessionid: input.sessionid } : {}),
|
|
357
|
-
body: eventpostbody({
|
|
358
|
-
event: {
|
|
359
|
-
kind: "progress",
|
|
360
|
-
stream: "mcpmode",
|
|
361
|
-
payload: progresseventpayload({ stepid: input.stepid, status: input.status }),
|
|
362
|
-
},
|
|
363
|
-
}),
|
|
364
|
-
});
|
|
365
|
-
return { envelope, wire: `${frameof(envelope)}\n` };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Builds the sampling request frame the serve mode sends to the client model: the prompt, the optional system text, the granted page content and the granted maximum tokens ride the request the client answers. */
|
|
369
|
-
export function samplingframe(input: {
|
|
370
|
-
clientid: string;
|
|
371
|
-
capabilities?: capabilityset;
|
|
372
|
-
prompt: string;
|
|
373
|
-
system?: string;
|
|
374
|
-
pagecontent?: string;
|
|
375
|
-
pagegrant?: boolean;
|
|
376
|
-
maxtokens?: number;
|
|
377
|
-
now: number;
|
|
378
|
-
}): { request?: samplingrequest; frame?: jsonrpcframe; reason?: string } {
|
|
379
|
-
const requested = requestsampling({
|
|
380
|
-
clientid: input.clientid,
|
|
381
|
-
...(input.capabilities !== undefined ? { capabilities: input.capabilities } : {}),
|
|
382
|
-
prompt: input.prompt,
|
|
383
|
-
...(input.system !== undefined ? { system: input.system } : {}),
|
|
384
|
-
...(input.pagecontent !== undefined ? { pagecontent: input.pagecontent } : {}),
|
|
385
|
-
...(input.pagegrant !== undefined ? { pagegrant: input.pagegrant } : {}),
|
|
386
|
-
...(input.maxtokens !== undefined ? { maxtokens: input.maxtokens } : {}),
|
|
387
|
-
now: input.now,
|
|
388
|
-
});
|
|
389
|
-
if (requested.request === undefined)
|
|
390
|
-
return { ...(requested.reason !== undefined ? { reason: requested.reason } : {}) };
|
|
391
|
-
return {
|
|
392
|
-
request: requested.request,
|
|
393
|
-
frame: {
|
|
394
|
-
jsonrpc: "2.0",
|
|
395
|
-
id: requested.request.id,
|
|
396
|
-
method: "sampling/create",
|
|
397
|
-
params: {
|
|
398
|
-
id: requested.request.id,
|
|
399
|
-
prompt: requested.request.prompt,
|
|
400
|
-
...(requested.request.system !== undefined ? { system: requested.request.system } : {}),
|
|
401
|
-
...(requested.request.pagecontent !== undefined ? { pagecontent: requested.request.pagecontent } : {}),
|
|
402
|
-
...(requested.request.maxtokens !== undefined ? { maxtokens: requested.request.maxtokens } : {}),
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/** Builds the stream chunk notification frames of one progressive tool result so a client reads the partial results as they arrive. */
|
|
409
|
-
export function chunkframes(chunks: streamchunk[]): jsonrpcframe[] {
|
|
410
|
-
return chunks.map((chunk) =>
|
|
411
|
-
notificationframe("calls/streamchunk", {
|
|
412
|
-
callid: chunk.callid,
|
|
413
|
-
seq: chunk.seq,
|
|
414
|
-
content: chunk.content,
|
|
415
|
-
done: chunk.done,
|
|
416
|
-
at: chunk.at,
|
|
417
|
-
}),
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/** Builds the progress notification frame of one long tool call with its cancel hint. */
|
|
422
|
-
export function progressframe(notice: progressnotice): jsonrpcframe {
|
|
423
|
-
return notificationframe("calls/progress", {
|
|
424
|
-
callid: notice.callid,
|
|
425
|
-
...(notice.percent !== undefined ? { percent: notice.percent } : {}),
|
|
426
|
-
message: notice.message,
|
|
427
|
-
cancellable: notice.cancellable,
|
|
428
|
-
at: notice.at,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** Emits one stream chunk of a progressive result beside its notification frame. */
|
|
433
|
-
export function chunkof(input: { callid: string; seq: number; content: string; done?: boolean; now: number }): {
|
|
434
|
-
chunk: streamchunk;
|
|
435
|
-
frame: jsonrpcframe;
|
|
436
|
-
} {
|
|
437
|
-
const chunk = streamchunkof(input);
|
|
438
|
-
return { chunk, frame: chunkframes([chunk])[0] as jsonrpcframe };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/** Emits one progress notice of a long call beside its notification frame. */
|
|
442
|
-
export function progressof(input: {
|
|
443
|
-
callid: string;
|
|
444
|
-
percent?: number;
|
|
445
|
-
message: string;
|
|
446
|
-
cancellable?: boolean;
|
|
447
|
-
now: number;
|
|
448
|
-
}): { notice: progressnotice; frame: jsonrpcframe } {
|
|
449
|
-
const notice = notifyprogress(input);
|
|
450
|
-
return { notice, frame: progressframe(notice) };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/** Builds the serve audit entry of one tool call: the caller client, the tool, the origin and the outcome without any payload. */
|
|
454
|
-
export function servecallevent(input: {
|
|
455
|
-
client: clientrecord;
|
|
456
|
-
tool: string;
|
|
457
|
-
origin: string;
|
|
458
|
-
ok: boolean;
|
|
459
|
-
now: number;
|
|
460
|
-
code?: rpcerrorcode;
|
|
461
|
-
callid?: string;
|
|
462
|
-
idempotencykey?: string;
|
|
463
|
-
dryrun?: boolean;
|
|
464
|
-
mocked?: boolean;
|
|
465
|
-
batchid?: string;
|
|
466
|
-
replayed?: boolean;
|
|
467
|
-
}): toolcallrecord {
|
|
468
|
-
return {
|
|
469
|
-
id: randomid(),
|
|
470
|
-
clientid: input.client.id,
|
|
471
|
-
tool: input.tool,
|
|
472
|
-
origin: input.origin,
|
|
473
|
-
ok: input.ok,
|
|
474
|
-
...(input.code !== undefined ? { code: input.code } : {}),
|
|
475
|
-
at: input.now,
|
|
476
|
-
...(input.callid !== undefined ? { callid: input.callid } : {}),
|
|
477
|
-
...(input.idempotencykey !== undefined && input.idempotencykey.trim() !== ""
|
|
478
|
-
? { idempotencykey: input.idempotencykey }
|
|
479
|
-
: {}),
|
|
480
|
-
...(input.dryrun === true ? { dryrun: true } : {}),
|
|
481
|
-
...(input.mocked === true ? { mocked: true } : {}),
|
|
482
|
-
...(input.batchid !== undefined ? { batchid: input.batchid } : {}),
|
|
483
|
-
...(input.replayed === true ? { replayed: true } : {}),
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/** Renders the audit line of one serve tool call: the caller name and version beside the tool, the origin and the outcome so the trail names the exact client build. */
|
|
488
|
-
export function auditline(record: toolcallrecord, client: clientrecord | undefined): string {
|
|
489
|
-
const caller =
|
|
490
|
-
client !== undefined
|
|
491
|
-
? `${client.name ?? client.id}${client.clientversion !== undefined ? ` (${client.clientversion})` : ""}`
|
|
492
|
-
: record.clientid;
|
|
493
|
-
return `The client ${caller} called ${record.tool} on ${record.origin} with the outcome ${record.ok ? "ok" : `refused (${String(record.code ?? "internal")})`}${record.dryrun === true ? " as a dry run" : ""}${record.mocked === true ? " through a test mock" : ""}${record.replayed === true ? " as an idempotent replay" : ""}${record.batchid !== undefined ? ` in the batch ${record.batchid}` : ""}.`;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/** Wraps one tool result with the structured details the sidepanel steps read: the step id, kind, risk and summary ride the payload so a client reads the same evidence the panels render. */
|
|
497
|
-
export function stepdetails(step: toolstep, result: toolresult): toolresult {
|
|
498
|
-
return {
|
|
499
|
-
...result,
|
|
500
|
-
payload: { ...(result.payload ?? {}), stepid: step.id, kind: step.kind, risk: step.risk, summary: step.summary },
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/** The outcome of one serve tool call: the response frame the transport writes, the audit record the trail keeps and the state updates the caller persists. */
|
|
505
|
-
export interface calloutcome {
|
|
506
|
-
response?: jsonrpcframe;
|
|
507
|
-
record?: toolcallrecord;
|
|
508
|
-
blocked?: boolean;
|
|
509
|
-
approval?: approvalrequest;
|
|
510
|
-
approvals?: approvalrequest[];
|
|
511
|
-
contexts?: callcontext[];
|
|
512
|
-
records?: idempotencyrecord[];
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/** Runs one serve tool call through the full composition: the mock seam answers test contexts without touching the browser, the dry run flag evaluates arguments and consent with no side effects, the idempotency key replays the stored result of a repeated call, the rate limit and the allowlist admit the client, the consent gates refuse what the session does not grant, a sensitive tool with the approval requirement raises the human gate and blocks until it answers, and every executed call returns the structured step details with its audit record. */
|
|
516
|
-
export async function dispatchcall(input: {
|
|
517
|
-
params?: Record<string, unknown>;
|
|
518
|
-
id?: number | string | null;
|
|
519
|
-
client: clientrecord;
|
|
520
|
-
catalog: toolcatalog;
|
|
521
|
-
session?: agentsession;
|
|
522
|
-
plan?: agentplan;
|
|
523
|
-
origin: string;
|
|
524
|
-
now: number;
|
|
525
|
-
scopes?: toolnamespace[];
|
|
526
|
-
readonly?: boolean;
|
|
527
|
-
mocks?: toolmock[];
|
|
528
|
-
idempotency?: idempotencyrecord[];
|
|
529
|
-
idempotencywindow?: number;
|
|
530
|
-
limits?: callratelimit[];
|
|
531
|
-
allowlist?: allowlistentry[];
|
|
532
|
-
approvals?: approvalrequest[];
|
|
533
|
-
approvaltimeout?: number;
|
|
534
|
-
contexts?: callcontext[];
|
|
535
|
-
execute: (step: toolstep) => toolresult | Promise<toolresult>;
|
|
536
|
-
}): Promise<calloutcome> {
|
|
537
|
-
const params = input.params;
|
|
538
|
-
const id = input.id;
|
|
539
|
-
const answer = (result: unknown): calloutcome => ({
|
|
540
|
-
response: respond({ ...(id !== undefined ? { id } : {}), result }),
|
|
541
|
-
});
|
|
542
|
-
const fail = (code: rpcerrorcode, message: string): calloutcome => ({
|
|
543
|
-
response: respond({ ...(id !== undefined ? { id } : {}), error: rpcerrorof(code, message) }),
|
|
544
|
-
});
|
|
545
|
-
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
546
|
-
return fail("params", "The tool call needs its params object.");
|
|
547
|
-
const name = typeof params.name === "string" ? params.name.trim() : "";
|
|
548
|
-
if (name === "") return fail("params", "The tool call needs the namespaced name of the tool it invokes.");
|
|
549
|
-
const tool = resolvetool(input.catalog, name);
|
|
550
|
-
if (tool === undefined) return fail("params", `The catalog holds no unambiguous tool named ${name}.`);
|
|
551
|
-
if (input.readonly === true && tool.risk !== "read")
|
|
552
|
-
return fail("consentrefused", `The serve runs degraded to the read only tools and refuses ${name}.`);
|
|
553
|
-
const namespace = namespaceof(tool.name);
|
|
554
|
-
if (input.scopes !== undefined && namespace !== undefined && !input.scopes.includes(namespace))
|
|
555
|
-
return fail("consentrefused", `The session token grants no ${namespace} tools.`);
|
|
556
|
-
const key =
|
|
557
|
-
typeof params.idempotencykey === "string" && params.idempotencykey.trim() !== ""
|
|
558
|
-
? params.idempotencykey
|
|
559
|
-
: undefined;
|
|
560
|
-
if (key !== undefined) {
|
|
561
|
-
const replay = checkidempotency({
|
|
562
|
-
records: input.idempotency ?? [],
|
|
563
|
-
key,
|
|
564
|
-
clientid: input.client.id,
|
|
565
|
-
now: input.now,
|
|
566
|
-
});
|
|
567
|
-
if (replay.replay !== undefined) {
|
|
568
|
-
const record = servecallevent({
|
|
569
|
-
client: input.client,
|
|
570
|
-
tool: tool.name,
|
|
571
|
-
origin: input.origin,
|
|
572
|
-
ok: !replay.replay.iserror,
|
|
573
|
-
now: input.now,
|
|
574
|
-
idempotencykey: key,
|
|
575
|
-
replayed: true,
|
|
576
|
-
});
|
|
577
|
-
return { ...answer(replay.replay), record };
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
const mocked = applymock({ mocks: input.mocks ?? [], tool: tool.name });
|
|
581
|
-
if (mocked.result !== undefined) {
|
|
582
|
-
const record = servecallevent({
|
|
583
|
-
client: input.client,
|
|
584
|
-
tool: tool.name,
|
|
585
|
-
origin: input.origin,
|
|
586
|
-
ok: !mocked.result.iserror,
|
|
587
|
-
now: input.now,
|
|
588
|
-
mocked: true,
|
|
589
|
-
});
|
|
590
|
-
return { ...answer(mocked.result), record };
|
|
591
|
-
}
|
|
592
|
-
if (params.dryrun === true) {
|
|
593
|
-
const dryrun = dryruntool({
|
|
594
|
-
tool,
|
|
595
|
-
params,
|
|
596
|
-
client: input.client,
|
|
597
|
-
...(input.session !== undefined ? { session: input.session } : {}),
|
|
598
|
-
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
599
|
-
origin: input.origin,
|
|
600
|
-
...(typeof params.stepid === "string" ? { stepid: params.stepid } : {}),
|
|
601
|
-
now: input.now,
|
|
602
|
-
});
|
|
603
|
-
const ok = dryrun.argsvalid && dryrun.consentok;
|
|
604
|
-
const record = servecallevent({
|
|
605
|
-
client: input.client,
|
|
606
|
-
tool: tool.name,
|
|
607
|
-
origin: input.origin,
|
|
608
|
-
ok,
|
|
609
|
-
now: input.now,
|
|
610
|
-
...(ok ? {} : { code: "consentrefused" as rpcerrorcode }),
|
|
611
|
-
dryrun: true,
|
|
612
|
-
});
|
|
613
|
-
return {
|
|
614
|
-
response: respond({
|
|
615
|
-
...(id !== undefined ? { id } : {}),
|
|
616
|
-
result: { content: JSON.stringify(dryrun), iserror: !ok },
|
|
617
|
-
}),
|
|
618
|
-
record,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
if (input.limits !== undefined && input.limits.length > 0) {
|
|
622
|
-
const limited = applyratelimit({ limits: input.limits, clientid: input.client.id, now: input.now });
|
|
623
|
-
if (!limited.allowed)
|
|
624
|
-
return fail(
|
|
625
|
-
"params",
|
|
626
|
-
`The client exhausted its call budget of ${String(limited.budget ?? 0)} for the ${String(limited.limit?.windowms ?? 0)} millisecond window; retry after ${String(limited.retryafter ?? 0)} milliseconds.`,
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
if (input.client.fingerprint !== undefined && input.allowlist !== undefined && input.allowlist.length > 0) {
|
|
630
|
-
const admitted = checkallowlist({
|
|
631
|
-
entries: input.allowlist,
|
|
632
|
-
fingerprint: input.client.fingerprint,
|
|
633
|
-
...(namespace !== undefined ? { namespace } : {}),
|
|
634
|
-
});
|
|
635
|
-
if (!admitted.allowed)
|
|
636
|
-
return fail("consentrefused", admitted.reason ?? "The client fingerprint stays outside the allowlist.");
|
|
637
|
-
}
|
|
638
|
-
const stepid = typeof params.stepid === "string" ? params.stepid : undefined;
|
|
639
|
-
const gate = tooldispatchgate({
|
|
640
|
-
client: input.client,
|
|
641
|
-
tool,
|
|
642
|
-
session: input.session,
|
|
643
|
-
plan: input.plan,
|
|
644
|
-
origin: input.origin,
|
|
645
|
-
...(stepid !== undefined ? { stepid } : {}),
|
|
646
|
-
now: input.now,
|
|
647
|
-
});
|
|
648
|
-
if (!gate.allowed) {
|
|
649
|
-
const record = servecallevent({
|
|
650
|
-
client: input.client,
|
|
651
|
-
tool: tool.name,
|
|
652
|
-
origin: input.origin,
|
|
653
|
-
ok: false,
|
|
654
|
-
now: input.now,
|
|
655
|
-
code: "consentrefused",
|
|
656
|
-
});
|
|
657
|
-
return { ...fail("consentrefused", gate.reason ?? "The consent gates refused the tool call."), record };
|
|
658
|
-
}
|
|
659
|
-
if (tool.risk !== "read" && tool.consentmeta?.approvalrequired === true) {
|
|
660
|
-
const approval = requireapproval({
|
|
661
|
-
clientid: input.client.id,
|
|
662
|
-
tool: tool.name,
|
|
663
|
-
reason: tool.consentmeta.review,
|
|
664
|
-
params,
|
|
665
|
-
now: input.now,
|
|
666
|
-
...(input.approvaltimeout !== undefined ? { timeout: input.approvaltimeout } : {}),
|
|
667
|
-
});
|
|
668
|
-
return {
|
|
669
|
-
blocked: true,
|
|
670
|
-
approval,
|
|
671
|
-
approvals: [...(input.approvals ?? []).filter((candidate) => candidate.id !== approval.id), approval],
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
const step =
|
|
675
|
-
tool.risk === "read"
|
|
676
|
-
? {
|
|
677
|
-
id: `mcp-${input.client.id}-${input.now}`,
|
|
678
|
-
kind: tool.kind,
|
|
679
|
-
summary: tool.description.split(".")[0] ?? tool.description,
|
|
680
|
-
risk: "read" as const,
|
|
681
|
-
...(typeof params.target === "string" ? { target: params.target } : {}),
|
|
682
|
-
...(typeof params.value === "string" ? { value: params.value } : {}),
|
|
683
|
-
...(params.options !== undefined && typeof params.options === "object" && !Array.isArray(params.options)
|
|
684
|
-
? { options: JSON.stringify(params.options) }
|
|
685
|
-
: {}),
|
|
686
|
-
}
|
|
687
|
-
: input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
688
|
-
if (step === undefined) return fail("consentrefused", "The tool call names a step the approved plan does not carry.");
|
|
689
|
-
const callid = `call-${input.now}-${randomid().slice(0, 6)}`;
|
|
690
|
-
const contexts = [
|
|
691
|
-
...(input.contexts ?? []),
|
|
692
|
-
begincall({
|
|
693
|
-
clientid: input.client.id,
|
|
694
|
-
tool: tool.name,
|
|
695
|
-
callid,
|
|
696
|
-
...(key !== undefined ? { idempotencykey: key } : {}),
|
|
697
|
-
now: input.now,
|
|
698
|
-
}),
|
|
699
|
-
];
|
|
700
|
-
let result: toolresult;
|
|
701
|
-
try {
|
|
702
|
-
result = await input.execute(step);
|
|
703
|
-
} catch (error) {
|
|
704
|
-
const closed = endcall({ contexts, callid, ok: false, errorcode: "internal", now: input.now });
|
|
705
|
-
return {
|
|
706
|
-
response: respond({
|
|
707
|
-
...(id !== undefined ? { id } : {}),
|
|
708
|
-
error: rpcerrorof("internal", error instanceof Error ? error.message : String(error)),
|
|
709
|
-
}),
|
|
710
|
-
contexts: closed.contexts,
|
|
711
|
-
record: servecallevent({
|
|
712
|
-
client: input.client,
|
|
713
|
-
tool: tool.name,
|
|
714
|
-
origin: input.origin,
|
|
715
|
-
ok: false,
|
|
716
|
-
now: input.now,
|
|
717
|
-
code: "internal",
|
|
718
|
-
callid,
|
|
719
|
-
}),
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
const closed = endcall({ contexts, callid, ok: !result.iserror, now: input.now });
|
|
723
|
-
const detailed = stepdetails(step, result);
|
|
724
|
-
const record = servecallevent({
|
|
725
|
-
client: input.client,
|
|
726
|
-
tool: tool.name,
|
|
727
|
-
origin: input.origin,
|
|
728
|
-
ok: !result.iserror,
|
|
729
|
-
now: input.now,
|
|
730
|
-
callid,
|
|
731
|
-
...(key !== undefined ? { idempotencykey: key } : {}),
|
|
732
|
-
});
|
|
733
|
-
const records =
|
|
734
|
-
key !== undefined
|
|
735
|
-
? recordidempotency({
|
|
736
|
-
records: input.idempotency ?? [],
|
|
737
|
-
key,
|
|
738
|
-
clientid: input.client.id,
|
|
739
|
-
tool: tool.name,
|
|
740
|
-
result: detailed,
|
|
741
|
-
now: input.now,
|
|
742
|
-
...(input.idempotencywindow !== undefined ? { window: input.idempotencywindow } : {}),
|
|
743
|
-
})
|
|
744
|
-
: input.idempotency;
|
|
745
|
-
return { ...answer(detailed), record, contexts: closed.contexts, ...(records !== undefined ? { records } : {}) };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/** Resumes one approval gated call after the human answered: an approved gate executes the held step with its structured details while a refused or expired gate never executes. */
|
|
749
|
-
export async function resumegatedcall(input: {
|
|
750
|
-
approval: approvalrequest;
|
|
751
|
-
client: clientrecord;
|
|
752
|
-
catalog: toolcatalog;
|
|
753
|
-
session?: agentsession;
|
|
754
|
-
plan?: agentplan;
|
|
755
|
-
origin: string;
|
|
756
|
-
now: number;
|
|
757
|
-
execute: (step: toolstep) => toolresult | Promise<toolresult>;
|
|
758
|
-
}): Promise<calloutcome> {
|
|
759
|
-
if (input.approval.state !== "approved") {
|
|
760
|
-
return {
|
|
761
|
-
response: respond({
|
|
762
|
-
error: rpcerrorof(
|
|
763
|
-
"consentrefused",
|
|
764
|
-
`The approval gate ${input.approval.id} answered ${input.approval.state} and the call never executes.`,
|
|
765
|
-
),
|
|
766
|
-
}),
|
|
767
|
-
record: servecallevent({
|
|
768
|
-
client: input.client,
|
|
769
|
-
tool: input.approval.tool,
|
|
770
|
-
origin: input.origin,
|
|
771
|
-
ok: false,
|
|
772
|
-
now: input.now,
|
|
773
|
-
code: "consentrefused",
|
|
774
|
-
}),
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
const tool = resolvetool(input.catalog, input.approval.tool);
|
|
778
|
-
if (tool === undefined)
|
|
779
|
-
return {
|
|
780
|
-
response: respond({
|
|
781
|
-
error: rpcerrorof("params", `The catalog holds no unambiguous tool named ${input.approval.tool}.`),
|
|
782
|
-
}),
|
|
783
|
-
};
|
|
784
|
-
const stepid = typeof input.approval.params.stepid === "string" ? input.approval.params.stepid : undefined;
|
|
785
|
-
const gate = tooldispatchgate({
|
|
786
|
-
client: input.client,
|
|
787
|
-
tool,
|
|
788
|
-
session: input.session,
|
|
789
|
-
plan: input.plan,
|
|
790
|
-
origin: input.origin,
|
|
791
|
-
...(stepid !== undefined ? { stepid } : {}),
|
|
792
|
-
now: input.now,
|
|
793
|
-
});
|
|
794
|
-
if (!gate.allowed) {
|
|
795
|
-
return {
|
|
796
|
-
response: respond({
|
|
797
|
-
error: rpcerrorof("consentrefused", gate.reason ?? "The consent gates refused the tool call."),
|
|
798
|
-
}),
|
|
799
|
-
record: servecallevent({
|
|
800
|
-
client: input.client,
|
|
801
|
-
tool: tool.name,
|
|
802
|
-
origin: input.origin,
|
|
803
|
-
ok: false,
|
|
804
|
-
now: input.now,
|
|
805
|
-
code: "consentrefused",
|
|
806
|
-
}),
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
const step = input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
810
|
-
if (step === undefined)
|
|
811
|
-
return {
|
|
812
|
-
response: respond({
|
|
813
|
-
error: rpcerrorof("consentrefused", "The approved gate names no step the approved plan carries."),
|
|
814
|
-
}),
|
|
815
|
-
};
|
|
816
|
-
const result = await input.execute(step);
|
|
817
|
-
return {
|
|
818
|
-
response: respond({
|
|
819
|
-
...(input.approval.params.id !== undefined ? { id: input.approval.params.id as number | string } : {}),
|
|
820
|
-
result: stepdetails(step, result),
|
|
821
|
-
}),
|
|
822
|
-
record: servecallevent({
|
|
823
|
-
client: input.client,
|
|
824
|
-
tool: tool.name,
|
|
825
|
-
origin: input.origin,
|
|
826
|
-
ok: !result.iserror,
|
|
827
|
-
now: input.now,
|
|
828
|
-
}),
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/** Runs one ordered batch of tool calls in a single request: every member dispatches through the caller composition while the stop on first error flag halts the batch at its first failure; the batch record grades through its members and lands with its per call outcomes. */
|
|
833
|
-
export async function runcallsbatch(input: {
|
|
834
|
-
calls: Array<{ id: string; name: string; params: Record<string, unknown> }>;
|
|
835
|
-
client: clientrecord;
|
|
836
|
-
now: number;
|
|
837
|
-
stoponerror: boolean;
|
|
838
|
-
execute: (
|
|
839
|
-
call: { id: string; name: string; params: Record<string, unknown> },
|
|
840
|
-
index: number,
|
|
841
|
-
) => Promise<{ ok: boolean; result?: toolresult; error?: structurederror }>;
|
|
842
|
-
}): Promise<{ batch: batchcall; outcomes: batchoutcome[] }> {
|
|
843
|
-
const batchid = `batch-${input.now}-${randomid().slice(0, 6)}`;
|
|
844
|
-
const batch: batchcall = {
|
|
845
|
-
id: batchid,
|
|
846
|
-
clientid: input.client.id,
|
|
847
|
-
calls: input.calls.map((call) => ({ id: call.id, name: call.name, params: call.params })),
|
|
848
|
-
stoponerror: input.stoponerror,
|
|
849
|
-
state: "running",
|
|
850
|
-
createdat: input.now,
|
|
851
|
-
outcomes: [],
|
|
852
|
-
};
|
|
853
|
-
const outcome = await runbatch({
|
|
854
|
-
calls: input.calls,
|
|
855
|
-
stoponerror: input.stoponerror,
|
|
856
|
-
now: input.now,
|
|
857
|
-
execute: input.execute,
|
|
858
|
-
});
|
|
859
|
-
return {
|
|
860
|
-
batch: {
|
|
861
|
-
...batch,
|
|
862
|
-
state: outcome.stoppedat !== undefined ? "stopped" : "done",
|
|
863
|
-
finishedat: input.now,
|
|
864
|
-
outcomes: outcome.outcomes,
|
|
865
|
-
},
|
|
866
|
-
outcomes: outcome.outcomes,
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/** The serve session state one frame routes through: the client, the session and plan the gates read, the served surfaces and the stores the composition updates. */
|
|
871
|
-
export interface servesession {
|
|
872
|
-
config: mcpserverconfig;
|
|
873
|
-
catalog: toolcatalog;
|
|
874
|
-
templates: prompttemplate[];
|
|
875
|
-
client: clientrecord;
|
|
876
|
-
session?: agentsession;
|
|
877
|
-
plan?: agentplan;
|
|
878
|
-
origin: string;
|
|
879
|
-
bindings: clientbinding[];
|
|
880
|
-
approvals: approvalrequest[];
|
|
881
|
-
watches: resourcewatch[];
|
|
882
|
-
contexts: callcontext[];
|
|
883
|
-
idempotency: idempotencyrecord[];
|
|
884
|
-
limits: callratelimit[];
|
|
885
|
-
mocks: toolmock[];
|
|
886
|
-
allowlist: allowlistentry[];
|
|
887
|
-
pagestate?: Record<string, unknown>;
|
|
888
|
-
audit?: auditevent[];
|
|
889
|
-
startedat: number;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/** Routes one serve frame through the extended method table: the initialize handshake records the client metadata, the tool listing respects the degradation and the negotiated namespaces, the tool calls run the full composition, the prompts and resources serve behind their exposure gates, the batches run their ordered members and the health answers its resource; every response keeps the request id. */
|
|
893
|
-
export async function routeserveframe(input: {
|
|
894
|
-
frame: jsonrpcframe;
|
|
895
|
-
state: servesession;
|
|
896
|
-
now: number;
|
|
897
|
-
execute: (step: toolstep) => toolresult | Promise<toolresult>;
|
|
898
|
-
}): Promise<{ response?: jsonrpcframe; notifications?: jsonrpcframe[]; state: servesession }> {
|
|
899
|
-
const frame = input.frame;
|
|
900
|
-
const state = input.state;
|
|
901
|
-
const id = frame.id;
|
|
902
|
-
const params = frame.params;
|
|
903
|
-
const answer = (result: unknown): { response: jsonrpcframe; state: servesession } => ({
|
|
904
|
-
response: respond({ ...(id !== undefined ? { id } : {}), result }),
|
|
905
|
-
state,
|
|
906
|
-
});
|
|
907
|
-
const fail = (error: rpcerror): { response: jsonrpcframe; state: servesession } => ({
|
|
908
|
-
response: respond({ ...(id !== undefined ? { id } : {}), error }),
|
|
909
|
-
state,
|
|
910
|
-
});
|
|
911
|
-
if (frame.method === undefined) return fail(rpcerrorof("method", "The serve frame carries no method to route."));
|
|
912
|
-
const entry = servemethods().find((candidate) => candidate.method === frame.method);
|
|
913
|
-
if (entry === undefined) return fail(rpcerrorof("method", `The serve mode routes no method named ${frame.method}.`));
|
|
914
|
-
if (entry.handler === "initialize") {
|
|
915
|
-
const handshake = serveinitialize({
|
|
916
|
-
...(params !== undefined ? { params } : {}),
|
|
917
|
-
config: state.config,
|
|
918
|
-
catalog: state.catalog,
|
|
919
|
-
templates: state.templates,
|
|
920
|
-
});
|
|
921
|
-
return {
|
|
922
|
-
response: respond({
|
|
923
|
-
...(id !== undefined ? { id } : {}),
|
|
924
|
-
result: {
|
|
925
|
-
serverinfo: handshake.handshake.serverinfo,
|
|
926
|
-
protocolversion: handshake.handshake.protocolversion,
|
|
927
|
-
servercontractversion: handshake.handshake.servercontractversion,
|
|
928
|
-
instructions: handshake.handshake.instructions,
|
|
929
|
-
},
|
|
930
|
-
}),
|
|
931
|
-
state: { ...state, client: handshake.client(state.client) },
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
if (entry.handler === "ping") return answer(ping({ now: input.now }));
|
|
935
|
-
if (entry.handler === "health")
|
|
936
|
-
return answer(healthof({ startedat: state.startedat, now: input.now, state: "running" }));
|
|
937
|
-
if (entry.handler === "listtools") {
|
|
938
|
-
const binding = state.bindings.find(
|
|
939
|
-
(candidate) => candidate.clientid === state.client.id && candidate.releasedat === undefined,
|
|
940
|
-
);
|
|
941
|
-
const degraded = binding?.readonly === true;
|
|
942
|
-
const scopes = state.client.capabilities?.namespaces;
|
|
943
|
-
const exposed = exposedtools(state.catalog, {
|
|
944
|
-
...(degraded ? { readonly: true } : {}),
|
|
945
|
-
...(scopes !== undefined ? { scopes } : {}),
|
|
946
|
-
});
|
|
947
|
-
return answer(listtools(exposed));
|
|
948
|
-
}
|
|
949
|
-
if (entry.handler === "negotiate") {
|
|
950
|
-
const server = servercapabilities({ config: state.config, catalog: state.catalog });
|
|
951
|
-
const clientcaps =
|
|
952
|
-
params?.capabilities && typeof params.capabilities === "object" && !Array.isArray(params.capabilities)
|
|
953
|
-
? (params.capabilities as Partial<capabilityset>)
|
|
954
|
-
: undefined;
|
|
955
|
-
const outcome = negotiate({ ...(clientcaps !== undefined ? { client: clientcaps } : {}), server });
|
|
956
|
-
return outcome.agreed
|
|
957
|
-
? answer(outcome.capabilities)
|
|
958
|
-
: fail(rpcerrorof("params", outcome.mismatch ?? "The capability negotiation did not agree."));
|
|
959
|
-
}
|
|
960
|
-
if (entry.handler === "listresources") {
|
|
961
|
-
const gate = resourceexposuregate({
|
|
962
|
-
client: state.client,
|
|
963
|
-
uri: "devthink://health",
|
|
964
|
-
served: servedresources().map((resource) => resource.uri),
|
|
965
|
-
});
|
|
966
|
-
if (!gate.allowed) return fail(rpcerrorof("consentrefused", gate.reason ?? "The resource exposure was refused."));
|
|
967
|
-
return answer(resourceslist());
|
|
968
|
-
}
|
|
969
|
-
if (entry.handler === "readresource") {
|
|
970
|
-
const uri = typeof params?.uri === "string" ? params.uri : "";
|
|
971
|
-
const gate = resourceexposuregate({
|
|
972
|
-
client: state.client,
|
|
973
|
-
uri,
|
|
974
|
-
served: servedresources().map((resource) => resource.uri),
|
|
975
|
-
});
|
|
976
|
-
if (!gate.allowed) return fail(rpcerrorof("consentrefused", gate.reason ?? "The resource exposure was refused."));
|
|
977
|
-
const read = readresource({
|
|
978
|
-
uri,
|
|
979
|
-
...(state.pagestate !== undefined ? { pagestate: state.pagestate } : {}),
|
|
980
|
-
...(state.plan !== undefined ? { plan: state.plan } : {}),
|
|
981
|
-
...(state.audit !== undefined ? { audit: state.audit } : {}),
|
|
982
|
-
...(state.session !== undefined ? { session: state.session } : {}),
|
|
983
|
-
health: healthof({ startedat: state.startedat, now: input.now, state: "running" }),
|
|
984
|
-
});
|
|
985
|
-
if (read.resource === undefined || read.text === undefined)
|
|
986
|
-
return fail(rpcerrorof("params", read.reason ?? "The resource read failed."));
|
|
987
|
-
return answer({ uri: read.resource.uri, mimetype: read.resource.mimetype, text: read.text });
|
|
988
|
-
}
|
|
989
|
-
if (entry.handler === "subscriberesource") {
|
|
990
|
-
const uri = typeof params?.uri === "string" ? params.uri : "";
|
|
991
|
-
const gate = resourceexposuregate({
|
|
992
|
-
client: state.client,
|
|
993
|
-
uri,
|
|
994
|
-
served: servedresources().map((resource) => resource.uri),
|
|
995
|
-
});
|
|
996
|
-
if (!gate.allowed) return fail(rpcerrorof("consentrefused", gate.reason ?? "The resource exposure was refused."));
|
|
997
|
-
const subscribed = subscriberesource({
|
|
998
|
-
clientid: state.client.id,
|
|
999
|
-
uri,
|
|
1000
|
-
...(state.pagestate !== undefined && uri === "devthink://pagestate" ? { state: state.pagestate } : {}),
|
|
1001
|
-
now: input.now,
|
|
1002
|
-
});
|
|
1003
|
-
if (subscribed.watch === undefined)
|
|
1004
|
-
return fail(rpcerrorof("params", subscribed.reason ?? "The resource subscription did not open."));
|
|
1005
|
-
return {
|
|
1006
|
-
response: respond({ ...(id !== undefined ? { id } : {}), result: { watchid: subscribed.watch.id, uri } }),
|
|
1007
|
-
state: { ...state, watches: [subscribed.watch, ...state.watches] },
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
if (entry.handler === "unsubscriberesource") {
|
|
1011
|
-
const watchid = typeof params?.watchid === "string" ? params.watchid : "";
|
|
1012
|
-
if (watchid === "") return fail(rpcerrorof("params", "The unsubscribe needs the watcher id."));
|
|
1013
|
-
return {
|
|
1014
|
-
response: respond({ ...(id !== undefined ? { id } : {}), result: { watchid, canceled: true } }),
|
|
1015
|
-
state: {
|
|
1016
|
-
...state,
|
|
1017
|
-
watches: state.watches.map((watch) =>
|
|
1018
|
-
watch.id === watchid && watch.canceledat === undefined ? { ...watch, canceledat: input.now } : watch,
|
|
1019
|
-
),
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
if (entry.handler === "listprompts") {
|
|
1024
|
-
const gate = promptexposuregate(state.client);
|
|
1025
|
-
if (!gate.allowed) return fail(rpcerrorof("consentrefused", gate.reason ?? "The prompt exposure was refused."));
|
|
1026
|
-
return answer({ prompts: listmcpprompts(state.templates) });
|
|
1027
|
-
}
|
|
1028
|
-
if (entry.handler === "callprompt") {
|
|
1029
|
-
const gate = promptexposuregate(state.client);
|
|
1030
|
-
if (!gate.allowed) return fail(rpcerrorof("consentrefused", gate.reason ?? "The prompt exposure was refused."));
|
|
1031
|
-
const name = typeof params?.name === "string" ? params.name : "";
|
|
1032
|
-
const args =
|
|
1033
|
-
params?.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments)
|
|
1034
|
-
? (params.arguments as Record<string, unknown>)
|
|
1035
|
-
: undefined;
|
|
1036
|
-
const rendered = rendermcpprompt({ name, ...(args !== undefined ? { args } : {}), templates: state.templates });
|
|
1037
|
-
if (rendered.rendered === undefined || rendered.prompt === undefined)
|
|
1038
|
-
return fail(rpcerrorof("params", rendered.reason ?? "The prompt call did not render."));
|
|
1039
|
-
return answer({ prompt: rendered.prompt.name, rendered: rendered.rendered, arguments: rendered.prompt.arguments });
|
|
1040
|
-
}
|
|
1041
|
-
if (entry.handler === "cancel") {
|
|
1042
|
-
const callid = typeof params?.callid === "string" ? params.callid : "";
|
|
1043
|
-
if (callid.trim() === "") return fail(rpcerrorof("params", "The cancellation frame needs the call id it aborts."));
|
|
1044
|
-
const aborted = canceltool({
|
|
1045
|
-
contexts: state.contexts,
|
|
1046
|
-
callid,
|
|
1047
|
-
...(typeof params?.reason === "string" ? { reason: params.reason } : {}),
|
|
1048
|
-
now: input.now,
|
|
1049
|
-
});
|
|
1050
|
-
if (aborted.context === undefined)
|
|
1051
|
-
return fail(rpcerrorof("params", aborted.reason ?? "The cancellation frame named no in flight tool call."));
|
|
1052
|
-
return {
|
|
1053
|
-
response: respond({
|
|
1054
|
-
...(id !== undefined ? { id } : {}),
|
|
1055
|
-
result: {
|
|
1056
|
-
cancelled: true,
|
|
1057
|
-
callid,
|
|
1058
|
-
...(aborted.context.partial !== undefined ? { partial: aborted.context.partial } : {}),
|
|
1059
|
-
},
|
|
1060
|
-
}),
|
|
1061
|
-
state: { ...state, contexts: aborted.contexts },
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
if (entry.handler === "batch") {
|
|
1065
|
-
const rawcalls =
|
|
1066
|
-
params !== undefined && Array.isArray(params.calls) ? (params.calls as Array<Record<string, unknown>>) : [];
|
|
1067
|
-
const calls = rawcalls
|
|
1068
|
-
.filter((call) => call !== null && typeof call === "object" && typeof call.name === "string")
|
|
1069
|
-
.map((call, index) => ({
|
|
1070
|
-
id: typeof call.id === "string" ? call.id : `member-${index + 1}`,
|
|
1071
|
-
name: call.name as string,
|
|
1072
|
-
params:
|
|
1073
|
-
call.params !== undefined && typeof call.params === "object" && !Array.isArray(call.params)
|
|
1074
|
-
? (call.params as Record<string, unknown>)
|
|
1075
|
-
: {},
|
|
1076
|
-
}));
|
|
1077
|
-
if (calls.length === 0) return fail(rpcerrorof("params", "The batch call needs its ordered tool calls."));
|
|
1078
|
-
const run = await runcallsbatch({
|
|
1079
|
-
calls,
|
|
1080
|
-
client: state.client,
|
|
1081
|
-
now: input.now,
|
|
1082
|
-
stoponerror: params?.stoponerror !== false,
|
|
1083
|
-
execute: async (call) => {
|
|
1084
|
-
const outcome = await dispatchcall({
|
|
1085
|
-
params: { ...call.params, name: call.name },
|
|
1086
|
-
...(id !== undefined ? { id } : {}),
|
|
1087
|
-
client: state.client,
|
|
1088
|
-
catalog: state.catalog,
|
|
1089
|
-
...(state.session !== undefined ? { session: state.session } : {}),
|
|
1090
|
-
...(state.plan !== undefined ? { plan: state.plan } : {}),
|
|
1091
|
-
origin: state.origin,
|
|
1092
|
-
now: input.now,
|
|
1093
|
-
...(state.mocks.length > 0 ? { mocks: state.mocks } : {}),
|
|
1094
|
-
...(state.idempotency.length > 0 ? { idempotency: state.idempotency } : {}),
|
|
1095
|
-
...(state.limits.length > 0 ? { limits: state.limits } : {}),
|
|
1096
|
-
...(state.allowlist.length > 0 ? { allowlist: state.allowlist } : {}),
|
|
1097
|
-
approvals: state.approvals,
|
|
1098
|
-
contexts: state.contexts,
|
|
1099
|
-
execute: input.execute,
|
|
1100
|
-
});
|
|
1101
|
-
if (outcome.response?.error !== undefined)
|
|
1102
|
-
return {
|
|
1103
|
-
ok: false,
|
|
1104
|
-
error: { code: outcome.response.error.code, message: outcome.response.error.message, retryhint: "none" },
|
|
1105
|
-
};
|
|
1106
|
-
const result = outcome.response?.result as toolresult | undefined;
|
|
1107
|
-
return result !== undefined
|
|
1108
|
-
? { ok: !result.iserror, result }
|
|
1109
|
-
: {
|
|
1110
|
-
ok: false,
|
|
1111
|
-
error: { code: "internal", message: "The batch member produced no result.", retryhint: "none" },
|
|
1112
|
-
};
|
|
1113
|
-
},
|
|
1114
|
-
});
|
|
1115
|
-
return answer({ batchid: run.batch.id, state: run.batch.state, outcomes: run.outcomes });
|
|
1116
|
-
}
|
|
1117
|
-
const outcome = await dispatchcall({
|
|
1118
|
-
...(params !== undefined ? { params } : {}),
|
|
1119
|
-
...(id !== undefined ? { id } : {}),
|
|
1120
|
-
client: state.client,
|
|
1121
|
-
catalog: state.catalog,
|
|
1122
|
-
...(state.session !== undefined ? { session: state.session } : {}),
|
|
1123
|
-
...(state.plan !== undefined ? { plan: state.plan } : {}),
|
|
1124
|
-
origin: state.origin,
|
|
1125
|
-
now: input.now,
|
|
1126
|
-
...(state.bindings.find((binding) => binding.clientid === state.client.id && binding.releasedat === undefined)
|
|
1127
|
-
?.readonly === true
|
|
1128
|
-
? { readonly: true }
|
|
1129
|
-
: {}),
|
|
1130
|
-
...(state.mocks.length > 0 ? { mocks: state.mocks } : {}),
|
|
1131
|
-
...(state.idempotency.length > 0 ? { idempotency: state.idempotency } : {}),
|
|
1132
|
-
...(state.limits.length > 0 ? { limits: state.limits } : {}),
|
|
1133
|
-
...(state.allowlist.length > 0 ? { allowlist: state.allowlist } : {}),
|
|
1134
|
-
approvals: state.approvals,
|
|
1135
|
-
...(state.config.remoteaccess?.approvaltimeout?.windowms !== undefined
|
|
1136
|
-
? { approvaltimeout: state.config.remoteaccess.approvaltimeout.windowms }
|
|
1137
|
-
: {}),
|
|
1138
|
-
contexts: state.contexts,
|
|
1139
|
-
execute: input.execute,
|
|
1140
|
-
});
|
|
1141
|
-
return {
|
|
1142
|
-
...(outcome.response !== undefined ? { response: outcome.response } : {}),
|
|
1143
|
-
state: {
|
|
1144
|
-
...state,
|
|
1145
|
-
...(outcome.approvals !== undefined ? { approvals: outcome.approvals } : {}),
|
|
1146
|
-
...(outcome.contexts !== undefined ? { contexts: outcome.contexts } : {}),
|
|
1147
|
-
...(outcome.records !== undefined ? { idempotency: outcome.records } : {}),
|
|
1148
|
-
},
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
/** Publishes one resource change of the serve session to its subscribers: every watcher of the resource receives its delta notification frame while the session keeps the updated watches. */
|
|
1153
|
-
export function publishresourcechange(input: {
|
|
1154
|
-
state: servesession;
|
|
1155
|
-
uri: string;
|
|
1156
|
-
changed: Record<string, unknown>;
|
|
1157
|
-
now: number;
|
|
1158
|
-
}): { notifications: jsonrpcframe[]; state: servesession } {
|
|
1159
|
-
const published = publishchange({
|
|
1160
|
-
watches: input.state.watches,
|
|
1161
|
-
uri: input.uri,
|
|
1162
|
-
state: input.changed,
|
|
1163
|
-
now: input.now,
|
|
1164
|
-
});
|
|
1165
|
-
return {
|
|
1166
|
-
notifications: published.deliveries.map((delivery) => delivery.frame),
|
|
1167
|
-
state: { ...input.state, watches: published.watches },
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/** Builds one serve session over the default catalog and the empty stores: the caller threads the session through the frame router and persists the returned state. */
|
|
1172
|
-
export function createservesession(input: {
|
|
1173
|
-
config: mcpserverconfig;
|
|
1174
|
-
client: clientrecord;
|
|
1175
|
-
templates?: prompttemplate[];
|
|
1176
|
-
session?: agentsession;
|
|
1177
|
-
plan?: agentplan;
|
|
1178
|
-
origin: string;
|
|
1179
|
-
now: number;
|
|
1180
|
-
catalog?: toolcatalog;
|
|
1181
|
-
}): servesession {
|
|
1182
|
-
return {
|
|
1183
|
-
config: input.config,
|
|
1184
|
-
catalog: input.catalog ?? buildtoolcatalog(),
|
|
1185
|
-
templates: input.templates ?? [],
|
|
1186
|
-
client: input.client,
|
|
1187
|
-
...(input.session !== undefined ? { session: input.session } : {}),
|
|
1188
|
-
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
1189
|
-
origin: input.origin,
|
|
1190
|
-
bindings: [],
|
|
1191
|
-
approvals: [],
|
|
1192
|
-
watches: [],
|
|
1193
|
-
contexts: [],
|
|
1194
|
-
idempotency: [],
|
|
1195
|
-
limits: [],
|
|
1196
|
-
mocks: [],
|
|
1197
|
-
allowlist: [],
|
|
1198
|
-
startedat: input.now,
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
/** Names the health resource of the served catalog so the serve route and the docs share one entry point. */
|
|
1203
|
-
export function healthresourceof(): servedresource | undefined {
|
|
1204
|
-
return resourceof("devthink://health");
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
/* ── Merged from mcpserver.ts: the 1.1.88 consolidation interns the correlated mcpserver logic here, so no variation of the same file lives beside another. ── */
|
|
1208
|
-
|
|
1209
|
-
/**
|
|
1210
|
-
* Mcp server of the 1.1.54 and 1.1.55 agent protocol family.
|
|
1211
|
-
* Every server, transport and frame routing concern lives in this file: the json rpc frame grammar with newline delimited and http post envelopes, the parse and serialize round trips, the frame validation that rejects malformed ids, unknown methods and bad params, the rpc error codes with their json rpc number mapping, the initialize handshake with the server info, the ping keepalive, the tools/list report of every tool with its json schema inputs and full consent metadata, the capability negotiation over protocol version, tool compatibility floor and transports, the per client tool floor negotiation, the client records with their pairing state, the per client request queue serialization under the user configured depth, the consent gated tool dispatch that checks the namespace first so unknown tools fail fast and never bypasses review, the localhost bind with its documented default, the stdio bridge that launches through the native messaging host manifest, relays frames in both directions and restarts a dead client process, and the framed logs that never leak payloads.
|
|
1212
|
-
* The server stays pure: execution flows through the injected execute seam so tests run on plain fixtures, the frame size and queue depth stay user configured with no code ceiling, and no endpoint, provider or key is ever hardcoded.
|
|
1213
|
-
*/
|
|
1214
|
-
|
|
1215
|
-
/** The documented default bind address of the http listener: localhost only. */
|
|
1216
|
-
export const localhostbind = "127.0.0.1";
|
|
1217
|
-
|
|
1218
|
-
/** The documented default port of the http listener; any user configured port wins. */
|
|
1219
|
-
export const defaultmcpport = 7436;
|
|
1220
|
-
|
|
1221
|
-
/** The json rpc error numbers of the agent protocol: parse, method, params and internal keep the classic json rpc codes while consentrefused reserves -32001 for gate refusals. */
|
|
1222
|
-
export const rpcerrornumbers: Record<rpcerrorcode, number> = {
|
|
1223
|
-
parse: -32700,
|
|
1224
|
-
method: -32601,
|
|
1225
|
-
params: -32602,
|
|
1226
|
-
internal: -32603,
|
|
1227
|
-
consentrefused: -32001,
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
/** Builds one rpc error payload from its code name and message. */
|
|
1231
|
-
export function rpcerrorof(code: rpcerrorcode, message: string, data?: unknown): rpcerror {
|
|
1232
|
-
return { code, message, ...(data !== undefined ? { data } : {}) };
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
/** Maps one json rpc error number back to its agent protocol code name; classic numbers the protocol does not use stay undefined. */
|
|
1236
|
-
export function rpcerrorcodeof(number: number): rpcerrorcode | undefined {
|
|
1237
|
-
const entry = (Object.entries(rpcerrornumbers) as Array<[rpcerrorcode, number]>).find(
|
|
1238
|
-
([, value]) => value === number,
|
|
1239
|
-
);
|
|
1240
|
-
return entry?.[0];
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/** The user configured default server config: localhost bind, the documented default port, both transports allowed and the server off until the user enables it. */
|
|
1244
|
-
export function defaultmcpconfig(): mcpserverconfig {
|
|
1245
|
-
return { port: defaultmcpport, transports: ["stdio", "http"], enabled: false };
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
/** Unwraps one http post envelope around a frame; a bare frame passes through untouched. */
|
|
1249
|
-
function unwraphttppost(value: unknown): unknown {
|
|
1250
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1251
|
-
const candidate = value as Record<string, unknown>;
|
|
1252
|
-
if (
|
|
1253
|
-
candidate.transport === "http" &&
|
|
1254
|
-
candidate.frame &&
|
|
1255
|
-
typeof candidate.frame === "object" &&
|
|
1256
|
-
!Array.isArray(candidate.frame)
|
|
1257
|
-
)
|
|
1258
|
-
return candidate.frame;
|
|
1259
|
-
}
|
|
1260
|
-
return value;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
/** Parses one raw json rpc frame from the wire, bare or wrapped in an http post envelope; malformed json throws so the caller answers with the parse error. */
|
|
1264
|
-
export function parseframe(raw: string): jsonrpcframe {
|
|
1265
|
-
const parsed = unwraphttppost(JSON.parse(raw) as unknown);
|
|
1266
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1267
|
-
throw new Error("A json rpc frame must be an object.");
|
|
1268
|
-
return parsed as jsonrpcframe;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
/** Parses a newline delimited wire block into its frames; empty lines carry no frame. */
|
|
1272
|
-
export function parsewire(raw: string): jsonrpcframe[] {
|
|
1273
|
-
return raw
|
|
1274
|
-
.split("\n")
|
|
1275
|
-
.map((line) => line.trim())
|
|
1276
|
-
.filter((line) => line.length > 0)
|
|
1277
|
-
.map((line) => parseframe(line));
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
/** Serializes one frame to its compact json rpc wire form. */
|
|
1281
|
-
export function serializeframe(frame: jsonrpcframe): string {
|
|
1282
|
-
return JSON.stringify(frame);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/** Serializes one frame for the requested wire format: newline delimited blocks end in one newline while http post envelopes wrap the frame for a posted body. */
|
|
1286
|
-
export function wireformat(frame: jsonrpcframe, format: "newline" | "httppost"): string {
|
|
1287
|
-
return format === "newline" ? `${serializeframe(frame)}\n` : JSON.stringify({ transport: "http", frame });
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/** Validates one request frame against the routing table: the jsonrpc tag, well formed ids, known methods, object params and the user configured frame size; a clean frame returns undefined. */
|
|
1291
|
-
export function validateframe(
|
|
1292
|
-
frame: jsonrpcframe,
|
|
1293
|
-
methods: methodentry[],
|
|
1294
|
-
config?: mcpserverconfig,
|
|
1295
|
-
): rpcerror | undefined {
|
|
1296
|
-
if (frame.jsonrpc !== "2.0") return rpcerrorof("parse", "The frame must carry the jsonrpc 2.0 tag.");
|
|
1297
|
-
if (frame.id !== undefined && typeof frame.id !== "number" && typeof frame.id !== "string" && frame.id !== null)
|
|
1298
|
-
return rpcerrorof("parse", "The frame id must be a number, a string or null.");
|
|
1299
|
-
if (frame.method === undefined || frame.method.trim() === "")
|
|
1300
|
-
return rpcerrorof("method", "The frame carries no method to route.");
|
|
1301
|
-
if (!methods.some((entry) => entry.method === frame.method))
|
|
1302
|
-
return rpcerrorof("method", `The server routes no method named ${frame.method}.`);
|
|
1303
|
-
if (frame.params !== undefined && (typeof frame.params !== "object" || Array.isArray(frame.params)))
|
|
1304
|
-
return rpcerrorof("params", "The frame params must be an object.");
|
|
1305
|
-
if (config?.framesize !== undefined && serializeframe(frame).length > config.framesize)
|
|
1306
|
-
return rpcerrorof(
|
|
1307
|
-
"params",
|
|
1308
|
-
`The serialized frame exceeds the user configured frame size of ${config.framesize} characters.`,
|
|
1309
|
-
);
|
|
1310
|
-
return undefined;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
/** Builds one response frame that answers the request id with either a result or an error; error frames without an id answer null so the client can correlate the failure. */
|
|
1314
|
-
export function respond(input: { id?: number | string | null; result?: unknown; error?: rpcerror }): jsonrpcframe {
|
|
1315
|
-
return {
|
|
1316
|
-
jsonrpc: "2.0",
|
|
1317
|
-
...(input.id === undefined ? (input.error !== undefined ? { id: null } : {}) : { id: input.id }),
|
|
1318
|
-
...(input.error !== undefined ? { error: input.error } : { result: input.result }),
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
/** The mcp server routing table: every json rpc method with its plain language description and the internal handler its frames route to; the 1.1.56 family adds the prompt tools and the in flight cancellation. */
|
|
1323
|
-
export function servermethods(): methodentry[] {
|
|
1324
|
-
return [
|
|
1325
|
-
{
|
|
1326
|
-
method: "initialize",
|
|
1327
|
-
handler: "initialize",
|
|
1328
|
-
description: "Completes the mcp handshake and returns the server info.",
|
|
1329
|
-
},
|
|
1330
|
-
{ method: "ping", handler: "ping", description: "Answers keepalive frames with pong." },
|
|
1331
|
-
{
|
|
1332
|
-
method: "tools/list",
|
|
1333
|
-
handler: "listtools",
|
|
1334
|
-
description: "Returns every tool with its version and json schema inputs.",
|
|
1335
|
-
},
|
|
1336
|
-
{ method: "negotiate", handler: "negotiate", description: "Exchanges capability sets with the client." },
|
|
1337
|
-
{ method: "tools/call", handler: "dispatch", description: "Invokes one tool behind the consent gates." },
|
|
1338
|
-
{
|
|
1339
|
-
method: "prompts/list",
|
|
1340
|
-
handler: "listprompts",
|
|
1341
|
-
description: "Lists the prompt defs the server exposes as callable tools.",
|
|
1342
|
-
},
|
|
1343
|
-
{
|
|
1344
|
-
method: "prompts/call",
|
|
1345
|
-
handler: "callprompt",
|
|
1346
|
-
description: "Renders one prompt and returns its arguments as a tool call.",
|
|
1347
|
-
},
|
|
1348
|
-
{
|
|
1349
|
-
method: "calls/cancel",
|
|
1350
|
-
handler: "cancel",
|
|
1351
|
-
description: "Aborts one in flight tool call and preserves its partial result.",
|
|
1352
|
-
},
|
|
1353
|
-
];
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
/** Builds the capability set the server offers: the protocol version, the server identity, the tool compatibility floor, the tool count, the namespaces and the allowed transports. */
|
|
1357
|
-
export function servercapabilities(input: { config: mcpserverconfig; catalog: toolcatalog }): capabilityset {
|
|
1358
|
-
return {
|
|
1359
|
-
protocolversion,
|
|
1360
|
-
name: "devthink",
|
|
1361
|
-
version: protocolversion,
|
|
1362
|
-
toolversion: input.catalog.version,
|
|
1363
|
-
tools: alltools(input.catalog).length,
|
|
1364
|
-
namespaces: toolnamespaces,
|
|
1365
|
-
transports: input.config.transports,
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
/** Completes the mcp handshake: the server info, the protocol version the server speaks, the frozen protocolv2 major a new client answers as its default wire line and the instructions that state the consent model in plain language. */
|
|
1370
|
-
export function initialize(input: {
|
|
1371
|
-
params?: Record<string, unknown>;
|
|
1372
|
-
config: mcpserverconfig;
|
|
1373
|
-
catalog: toolcatalog;
|
|
1374
|
-
}): { serverinfo: capabilityset; protocolversion: string; protocolmajor: number; instructions: string } {
|
|
1375
|
-
void input.params;
|
|
1376
|
-
return {
|
|
1377
|
-
serverinfo: servercapabilities({ config: input.config, catalog: input.catalog }),
|
|
1378
|
-
protocolversion,
|
|
1379
|
-
protocolmajor: protocolmajorversion,
|
|
1380
|
-
instructions:
|
|
1381
|
-
"Devthink serves browser tools behind the human review gates: read only tools run once a session is approved while every tool with side effects executes exactly the approved plan step it names. No endpoint, provider or key is hardcoded; the user pairs every client.",
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
/** Answers one keepalive frame with pong. */
|
|
1386
|
-
export function ping(input: { now: number }): { pong: true; at: number } {
|
|
1387
|
-
return { pong: true, at: input.now };
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
/** Lists every tool of the catalog with its version, its json schema inputs, its risk grade and the full consent metadata of tools with side effects: the review requirement, the policy derived risk class, the approval gate requirement and the session origin scope. */
|
|
1391
|
-
export function listtools(catalog: toolcatalog): {
|
|
1392
|
-
tools: Array<{
|
|
1393
|
-
name: string;
|
|
1394
|
-
version: number;
|
|
1395
|
-
description: string;
|
|
1396
|
-
inputschema: toolcatalog["domains"][number]["tools"][number]["inputschema"];
|
|
1397
|
-
risk: string;
|
|
1398
|
-
consentmeta?: { review: string; riskclass: string; approvalrequired: boolean; originscope: string };
|
|
1399
|
-
}>;
|
|
1400
|
-
} {
|
|
1401
|
-
return {
|
|
1402
|
-
tools: alltools(catalog).map((tool) => ({
|
|
1403
|
-
name: tool.name,
|
|
1404
|
-
version: tool.version,
|
|
1405
|
-
description: tool.description,
|
|
1406
|
-
inputschema: tool.inputschema,
|
|
1407
|
-
risk: tool.risk,
|
|
1408
|
-
...(tool.consentmeta !== undefined
|
|
1409
|
-
? {
|
|
1410
|
-
consentmeta: {
|
|
1411
|
-
review: tool.consentmeta.review,
|
|
1412
|
-
riskclass: tool.consentmeta.riskclass ?? tool.risk,
|
|
1413
|
-
approvalrequired: tool.consentmeta.approvalrequired ?? true,
|
|
1414
|
-
originscope: tool.consentmeta.originscope ?? "session",
|
|
1415
|
-
},
|
|
1416
|
-
}
|
|
1417
|
-
: {}),
|
|
1418
|
-
})),
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
/** Collects the unknown top level fields of one wire frame: every key outside the json rpc grammar of jsonrpc, id, method, params, result and error names an unknown field the strict protocolv2 mode refuses before dispatch. */
|
|
1423
|
-
export function unknownframefields(frame: jsonrpcframe): string[] {
|
|
1424
|
-
return Object.keys(frame as unknown as Record<string, unknown>).filter(
|
|
1425
|
-
(key) =>
|
|
1426
|
-
key !== "jsonrpc" && key !== "id" && key !== "method" && key !== "params" && key !== "result" && key !== "error",
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
/** Reads the negotiated protocol major of one client: the client capability set declares the numeric protocolmajor and a client that declares nothing answers the protocolv2 default of two, so the strict schema modes follow the negotiated line. The deprecated full version string the version one clients exchanged left the capability set at the 2.0.0 sunset: the string form parses nowhere and a client that still sends it answers the strict refusal through the schema layer, never a negotiated major. */
|
|
1431
|
-
export function clientprotocolmajor(client: clientrecord): number {
|
|
1432
|
-
return protocolmajorof(client.capabilities?.protocolmajor) ?? protocolmajorversion;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
/** Exchanges capability sets with one client: protocol version, tool compatibility floor and transports must agree or the negotiation reports the mismatch; the 1.1.91 api freeze negotiates the protocol major as the 2.0.0 sunset left it — a client that declares nothing answers the frozen major two, the capability set declares the numeric protocolmajor (the deprecated full version string the version one clients exchanged left the wire at the sunset and parses nowhere), a major below the supported floor refuses with the migration path inside the mismatch, and a major above the supported range reports its mismatch with the supported range in plain language; the agreed set carries the negotiated floor. */
|
|
1436
|
-
export function negotiate(input: { client?: Partial<capabilityset>; server: capabilityset }): {
|
|
1437
|
-
agreed: boolean;
|
|
1438
|
-
mismatch?: string;
|
|
1439
|
-
capabilities?: capabilityset;
|
|
1440
|
-
protocolmajor?: number;
|
|
1441
|
-
deprecation?: string;
|
|
1442
|
-
} {
|
|
1443
|
-
const client = input.client;
|
|
1444
|
-
const negotiation = negotiateprotocol({
|
|
1445
|
-
...(client?.protocolmajor !== undefined ? { client: client.protocolmajor } : {}),
|
|
1446
|
-
});
|
|
1447
|
-
if (!negotiation.agreed)
|
|
1448
|
-
return { agreed: false, ...(negotiation.reason !== undefined ? { mismatch: negotiation.reason } : {}) };
|
|
1449
|
-
if (client?.toolversion !== undefined && client.toolversion > input.server.toolversion)
|
|
1450
|
-
return {
|
|
1451
|
-
agreed: false,
|
|
1452
|
-
mismatch: `The client requires tool version ${String(client.toolversion)} while the server offers ${String(input.server.toolversion)}.`,
|
|
1453
|
-
};
|
|
1454
|
-
if (
|
|
1455
|
-
client?.transports !== undefined &&
|
|
1456
|
-
client.transports.some((transport) => !input.server.transports.includes(transport))
|
|
1457
|
-
)
|
|
1458
|
-
return { agreed: false, mismatch: "The client requires a transport the server configuration does not allow." };
|
|
1459
|
-
return {
|
|
1460
|
-
agreed: true,
|
|
1461
|
-
capabilities: input.server,
|
|
1462
|
-
...(negotiation.major !== undefined ? { protocolmajor: negotiation.major } : {}),
|
|
1463
|
-
...(negotiation.deprecation !== undefined ? { deprecation: negotiation.deprecation } : {}),
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
/** Registers one connected client per transport; every client starts unpaired and waits for the user pairing approval. */
|
|
1468
|
-
export function connectclient(input: { transport: transportkind; now: number; id?: string }): clientrecord {
|
|
1469
|
-
return { id: input.id ?? `client-${input.now}`, transport: input.transport, paired: false, connectedat: input.now };
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
/** Applies one pairing decision: approval pairs the client while refusal leaves it unpaired and disconnected; already disconnected clients stay untouched. */
|
|
1473
|
-
export function pairclient(clients: clientrecord[], id: string, approved: boolean, now: number): clientrecord[] {
|
|
1474
|
-
return clients.map((client) =>
|
|
1475
|
-
client.id !== id || client.disconnectedat !== undefined
|
|
1476
|
-
? client
|
|
1477
|
-
: approved
|
|
1478
|
-
? { ...client, paired: true, pairedat: now }
|
|
1479
|
-
: { ...client, paired: false, disconnectedat: now },
|
|
1480
|
-
);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
/** Disconnects one client on demand; the record stays for the audit trail with its disconnect time. */
|
|
1484
|
-
export function disconnectclient(clients: clientrecord[], id: string, now: number): clientrecord[] {
|
|
1485
|
-
return clients.map((client) =>
|
|
1486
|
-
client.id === id && client.disconnectedat === undefined ? { ...client, disconnectedat: now } : client,
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
/** Enqueues one request frame for a client; frames serialize per client in arrival order and the user configured depth refuses the frame that would exceed it while an absent depth keeps the queue unbounded. */
|
|
1491
|
-
export function enqueuerequest(input: {
|
|
1492
|
-
queue: jsonrpcframe[];
|
|
1493
|
-
frame: jsonrpcframe;
|
|
1494
|
-
depth?: number;
|
|
1495
|
-
}): jsonrpcframe[] | undefined {
|
|
1496
|
-
if (input.depth !== undefined && input.queue.length + 1 > input.depth) return undefined;
|
|
1497
|
-
return [...input.queue, input.frame];
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/** Pops the next request frame of one client queue; an empty queue returns undefined. */
|
|
1501
|
-
export function nextrequest(queue: jsonrpcframe[]): { frame: jsonrpcframe; remaining: jsonrpcframe[] } | undefined {
|
|
1502
|
-
return queue.length === 0 ? undefined : { frame: queue[0] as jsonrpcframe, remaining: queue.slice(1) };
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
/** Negotiates the tool compatibility floor with one client: the client declares the highest floor it can parse and the catalog must satisfy it, so the negotiated floor is the client floor while a floor above the catalog version reports its mismatch in plain language. */
|
|
1506
|
-
export function negotiatetoolfloor(
|
|
1507
|
-
clientfloor: number | undefined,
|
|
1508
|
-
catalogversion: number,
|
|
1509
|
-
): { floor: number } | { mismatch: string } {
|
|
1510
|
-
if (clientfloor === undefined) return { floor: catalogversion };
|
|
1511
|
-
if (clientfloor > catalogversion)
|
|
1512
|
-
return {
|
|
1513
|
-
mismatch: `The client requires the tool version floor ${clientfloor} while the catalog serves version ${catalogversion}.`,
|
|
1514
|
-
};
|
|
1515
|
-
return { floor: clientfloor };
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
/** Dispatches one tool call behind the consent gates: the namespace check runs first so unknown tools fail fast, then the client must be paired, the session live, the plan approved, the origin inside the session grants and the tool version at or above the negotiated floor; read only tools run under the dryrun risk class while every tool with side effects executes exactly the approved plan step it names. */
|
|
1519
|
-
export async function dispatchtool(input: {
|
|
1520
|
-
params?: Record<string, unknown>;
|
|
1521
|
-
client: clientrecord;
|
|
1522
|
-
catalog: toolcatalog;
|
|
1523
|
-
session?: agentsession;
|
|
1524
|
-
plan?: agentplan;
|
|
1525
|
-
origin: string;
|
|
1526
|
-
now: number;
|
|
1527
|
-
scopes?: toolnamespace[];
|
|
1528
|
-
execute: (step: toolstep) => toolresult | Promise<toolresult>;
|
|
1529
|
-
}): Promise<{ result?: toolresult; error?: rpcerror; step?: toolstep }> {
|
|
1530
|
-
const params = input.params;
|
|
1531
|
-
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
1532
|
-
return { error: rpcerrorof("params", "The tool call needs its params object.") };
|
|
1533
|
-
if (typeof params.name !== "string" || !params.name.trim())
|
|
1534
|
-
return { error: rpcerrorof("params", "The tool call needs the namespaced name of the tool it invokes.") };
|
|
1535
|
-
const tool = resolvetool(input.catalog, params.name.trim());
|
|
1536
|
-
if (tool === undefined)
|
|
1537
|
-
return { error: rpcerrorof("params", `The catalog holds no unambiguous tool named ${params.name.trim()}.`) };
|
|
1538
|
-
const namespace = namespaceof(tool.name);
|
|
1539
|
-
if (namespace === undefined)
|
|
1540
|
-
return { error: rpcerrorof("params", `The tool ${tool.name} carries no reviewed namespace.`) };
|
|
1541
|
-
if (input.scopes !== undefined && !input.scopes.includes(namespace))
|
|
1542
|
-
return { error: rpcerrorof("consentrefused", `The session token grants no ${namespace} tools.`) };
|
|
1543
|
-
const floor = input.client.toolfloor ?? input.client.capabilities?.toolversion ?? input.catalog.version;
|
|
1544
|
-
if (tool.version < floor)
|
|
1545
|
-
return {
|
|
1546
|
-
error: rpcerrorof(
|
|
1547
|
-
"params",
|
|
1548
|
-
`The tool ${tool.name} of version ${tool.version} stays below the negotiated compatibility floor of ${floor}.`,
|
|
1549
|
-
),
|
|
1550
|
-
};
|
|
1551
|
-
const stepid = typeof params.stepid === "string" ? params.stepid : undefined;
|
|
1552
|
-
const gate = tooldispatchgate({
|
|
1553
|
-
client: input.client,
|
|
1554
|
-
tool,
|
|
1555
|
-
session: input.session,
|
|
1556
|
-
plan: input.plan,
|
|
1557
|
-
origin: input.origin,
|
|
1558
|
-
...(stepid !== undefined ? { stepid } : {}),
|
|
1559
|
-
now: input.now,
|
|
1560
|
-
});
|
|
1561
|
-
if (!gate.allowed)
|
|
1562
|
-
return { error: rpcerrorof("consentrefused", gate.reason ?? "The consent gates refused the tool call.") };
|
|
1563
|
-
const step =
|
|
1564
|
-
tool.risk === "read"
|
|
1565
|
-
? {
|
|
1566
|
-
id: `mcp-${input.client.id}-${input.now}`,
|
|
1567
|
-
kind: tool.kind,
|
|
1568
|
-
summary: tool.description.split(".")[0] ?? tool.description,
|
|
1569
|
-
risk: "read" as const,
|
|
1570
|
-
...(typeof params.target === "string" ? { target: params.target } : {}),
|
|
1571
|
-
...(typeof params.value === "string" ? { value: params.value } : {}),
|
|
1572
|
-
...(params.options !== undefined && typeof params.options === "object" && !Array.isArray(params.options)
|
|
1573
|
-
? { options: JSON.stringify(params.options) }
|
|
1574
|
-
: {}),
|
|
1575
|
-
}
|
|
1576
|
-
: input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
1577
|
-
if (step === undefined)
|
|
1578
|
-
return { error: rpcerrorof("consentrefused", "The tool call names a step the approved plan does not carry.") };
|
|
1579
|
-
try {
|
|
1580
|
-
const result = await input.execute(step);
|
|
1581
|
-
return { result, step };
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
return { error: rpcerrorof("internal", error instanceof Error ? error.message : String(error)) };
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
/** Parses, validates and routes one json rpc frame: parse failures answer with the parse error, validation failures echo the same id with their error, and clean frames reach the handler the routing table names — the 1.1.56 family routes the prompt tool listing, the prompt rendering and the in flight cancellation while the stateful subscription, resource, sampling and batch methods route through the frame intake with their persisted state; every response keeps the request id. */
|
|
1588
|
-
export async function handleframe(input: {
|
|
1589
|
-
raw?: string;
|
|
1590
|
-
frame?: jsonrpcframe;
|
|
1591
|
-
client: clientrecord;
|
|
1592
|
-
catalog: toolcatalog;
|
|
1593
|
-
config: mcpserverconfig;
|
|
1594
|
-
session?: agentsession;
|
|
1595
|
-
plan?: agentplan;
|
|
1596
|
-
origin: string;
|
|
1597
|
-
tabid: number;
|
|
1598
|
-
now: number;
|
|
1599
|
-
scopes?: toolnamespace[];
|
|
1600
|
-
contexts?: callcontext[];
|
|
1601
|
-
execute: (step: toolstep) => toolresult | Promise<toolresult>;
|
|
1602
|
-
}): Promise<jsonrpcframe> {
|
|
1603
|
-
if (input.raw !== undefined && input.config.framesize !== undefined && input.raw.length > input.config.framesize)
|
|
1604
|
-
return respond({
|
|
1605
|
-
id: null,
|
|
1606
|
-
error: rpcerrorof(
|
|
1607
|
-
"params",
|
|
1608
|
-
`The wire frame exceeds the user configured frame size of ${input.config.framesize} characters.`,
|
|
1609
|
-
),
|
|
1610
|
-
});
|
|
1611
|
-
let frame: jsonrpcframe;
|
|
1612
|
-
if (input.raw !== undefined) {
|
|
1613
|
-
try {
|
|
1614
|
-
frame = parseframe(input.raw);
|
|
1615
|
-
} catch {
|
|
1616
|
-
return respond({ id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") });
|
|
1617
|
-
}
|
|
1618
|
-
} else if (input.frame !== undefined) {
|
|
1619
|
-
frame = input.frame;
|
|
1620
|
-
} else {
|
|
1621
|
-
return respond({ id: null, error: rpcerrorof("parse", "The server received no frame to route.") });
|
|
1622
|
-
}
|
|
1623
|
-
const invalid = validateframe(frame, servermethods(), input.config);
|
|
1624
|
-
if (invalid !== undefined) return respond({ ...(frame.id !== undefined ? { id: frame.id } : {}), error: invalid });
|
|
1625
|
-
/* the 1.1.91 api freeze runs schema validation on every incoming message before dispatch: the strict refusal answers unknown frame fields on every major the line accepts — the 2.0.0 sunset closed the version one tolerance the deprecation window carried */
|
|
1626
|
-
const unknown = unknownframefields(frame);
|
|
1627
|
-
if (unknown.length > 0) {
|
|
1628
|
-
const strictness = unknownfieldsgate({ unknown, major: clientprotocolmajor(input.client) });
|
|
1629
|
-
if (!strictness.allowed)
|
|
1630
|
-
return respond({
|
|
1631
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1632
|
-
error: rpcerrorof(
|
|
1633
|
-
"params",
|
|
1634
|
-
strictness.reason ?? "The frame carries unknown fields the frozen schema declares nowhere.",
|
|
1635
|
-
),
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
const entry = servermethods().find((candidate) => candidate.method === frame.method);
|
|
1639
|
-
if (entry === undefined)
|
|
1640
|
-
return respond({
|
|
1641
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1642
|
-
error: rpcerrorof("method", `The server routes no method named ${String(frame.method)}.`),
|
|
1643
|
-
});
|
|
1644
|
-
const params = frame.params;
|
|
1645
|
-
if (entry.handler === "initialize")
|
|
1646
|
-
return respond({
|
|
1647
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1648
|
-
result: initialize({ ...(params !== undefined ? { params } : {}), config: input.config, catalog: input.catalog }),
|
|
1649
|
-
});
|
|
1650
|
-
if (entry.handler === "ping")
|
|
1651
|
-
return respond({ ...(frame.id !== undefined ? { id: frame.id } : {}), result: ping({ now: input.now }) });
|
|
1652
|
-
if (entry.handler === "listtools")
|
|
1653
|
-
return respond({ ...(frame.id !== undefined ? { id: frame.id } : {}), result: listtools(input.catalog) });
|
|
1654
|
-
if (entry.handler === "negotiate") {
|
|
1655
|
-
const server = servercapabilities({ config: input.config, catalog: input.catalog });
|
|
1656
|
-
const clientcaps =
|
|
1657
|
-
params?.capabilities && typeof params.capabilities === "object" && !Array.isArray(params.capabilities)
|
|
1658
|
-
? (params.capabilities as Partial<capabilityset>)
|
|
1659
|
-
: undefined;
|
|
1660
|
-
const outcome = negotiate({ ...(clientcaps !== undefined ? { client: clientcaps } : {}), server });
|
|
1661
|
-
return respond({
|
|
1662
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1663
|
-
...(outcome.agreed
|
|
1664
|
-
? { result: outcome.capabilities }
|
|
1665
|
-
: { error: rpcerrorof("params", outcome.mismatch ?? "The capability negotiation did not agree.") }),
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
if (entry.handler === "listprompts")
|
|
1669
|
-
return respond({ ...(frame.id !== undefined ? { id: frame.id } : {}), result: { prompts: listprompts() } });
|
|
1670
|
-
if (entry.handler === "callprompt") {
|
|
1671
|
-
const name = typeof params?.name === "string" ? params.name : "";
|
|
1672
|
-
const args =
|
|
1673
|
-
params?.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments)
|
|
1674
|
-
? (params.arguments as Record<string, unknown>)
|
|
1675
|
-
: undefined;
|
|
1676
|
-
const called =
|
|
1677
|
-
name === ""
|
|
1678
|
-
? { reason: "The prompt call needs the prompt name." }
|
|
1679
|
-
: callprompt({ name, ...(args !== undefined ? { args } : {}) });
|
|
1680
|
-
if (called.toolcall === undefined)
|
|
1681
|
-
return respond({
|
|
1682
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1683
|
-
error: rpcerrorof("params", called.reason ?? "The prompt call did not render."),
|
|
1684
|
-
});
|
|
1685
|
-
return respond({
|
|
1686
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1687
|
-
result: { toolcall: called.toolcall, rendered: called.rendered },
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
if (entry.handler === "cancel") {
|
|
1691
|
-
const callid = typeof params?.callid === "string" ? params.callid : "";
|
|
1692
|
-
const reason = typeof params?.reason === "string" ? params.reason : undefined;
|
|
1693
|
-
if (callid.trim() === "")
|
|
1694
|
-
return respond({
|
|
1695
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1696
|
-
error: rpcerrorof("params", "The cancellation frame needs the call id it aborts."),
|
|
1697
|
-
});
|
|
1698
|
-
const aborted = canceltool({
|
|
1699
|
-
contexts: input.contexts ?? [],
|
|
1700
|
-
callid,
|
|
1701
|
-
...(reason !== undefined ? { reason } : {}),
|
|
1702
|
-
now: input.now,
|
|
1703
|
-
});
|
|
1704
|
-
if (aborted.context === undefined)
|
|
1705
|
-
return respond({
|
|
1706
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1707
|
-
error: rpcerrorof("params", aborted.reason ?? "The cancellation frame named no in flight tool call."),
|
|
1708
|
-
});
|
|
1709
|
-
return respond({
|
|
1710
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1711
|
-
result: {
|
|
1712
|
-
cancelled: true,
|
|
1713
|
-
callid,
|
|
1714
|
-
...(reason !== undefined ? { reason } : {}),
|
|
1715
|
-
...(aborted.context.partial !== undefined ? { partial: aborted.context.partial } : {}),
|
|
1716
|
-
},
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
const dispatched = await dispatchtool({
|
|
1720
|
-
...(params !== undefined ? { params } : {}),
|
|
1721
|
-
client: input.client,
|
|
1722
|
-
catalog: input.catalog,
|
|
1723
|
-
...(input.session !== undefined ? { session: input.session } : {}),
|
|
1724
|
-
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
1725
|
-
...(input.scopes !== undefined ? { scopes: input.scopes } : {}),
|
|
1726
|
-
origin: input.origin,
|
|
1727
|
-
now: input.now,
|
|
1728
|
-
execute: input.execute,
|
|
1729
|
-
});
|
|
1730
|
-
return respond({
|
|
1731
|
-
...(frame.id !== undefined ? { id: frame.id } : {}),
|
|
1732
|
-
...(dispatched.error !== undefined ? { error: dispatched.error } : { result: dispatched.result }),
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
/** Normalizes the http listener bind of one server config: an absent bind keeps the documented localhost default and the report states whether the bind stays local. */
|
|
1737
|
-
export function bindlocalhost(config: mcpserverconfig): { bind: string; port: number; localhost: boolean } {
|
|
1738
|
-
const bind = config.bind !== undefined && config.bind.trim() !== "" ? config.bind.trim() : localhostbind;
|
|
1739
|
-
return { bind, port: config.port, localhost: bind === localhostbind || bind === "localhost" || bind === "::1" };
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
/** Launches the stdio bridge for one local client process through its native messaging host manifest; the host name is the manifest name and the process id rides the bridge record when the host reports it, because the browser itself never exposes the native host process id. The 1.1.95 hardening adds the client signature verification: a platform verifier that allows signature checks confirms the presented signature before the bridge connects, a failed signature refuses the bridge outright, and a platform without a verifier keeps the bridge unverified while the record says so. */
|
|
1743
|
-
export function launchbridge(input: {
|
|
1744
|
-
host: string;
|
|
1745
|
-
pid?: number;
|
|
1746
|
-
now: number;
|
|
1747
|
-
id?: string;
|
|
1748
|
-
signature?: string;
|
|
1749
|
-
verifier?: (host: string, signature: string) => boolean;
|
|
1750
|
-
}): stdiobridge {
|
|
1751
|
-
if (input.signature !== undefined && input.verifier !== undefined && !input.verifier(input.host, input.signature))
|
|
1752
|
-
throw new Error(
|
|
1753
|
-
`The client signature of the ${input.host} bridge failed the platform verification; the local stdio bridge refuses the client and no frame relays.`,
|
|
1754
|
-
);
|
|
1755
|
-
return {
|
|
1756
|
-
id: input.id ?? `bridge-${input.now}`,
|
|
1757
|
-
host: input.host,
|
|
1758
|
-
connected: true,
|
|
1759
|
-
...(input.pid !== undefined ? { pid: input.pid } : {}),
|
|
1760
|
-
startedat: input.now,
|
|
1761
|
-
restarts: 0,
|
|
1762
|
-
received: 0,
|
|
1763
|
-
sent: 0,
|
|
1764
|
-
...(input.signature !== undefined && input.verifier !== undefined
|
|
1765
|
-
? { clientverified: input.verifier(input.host, input.signature) }
|
|
1766
|
-
: {}),
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
/** Relays one frame between the local client process and the server: inbound frames arrive from the client wire, outbound frames return to it, and the bridge stamps its counters and the frame time. */
|
|
1771
|
-
export function relayframe(input: {
|
|
1772
|
-
bridge: stdiobridge;
|
|
1773
|
-
direction: "inbound" | "outbound";
|
|
1774
|
-
frame: jsonrpcframe;
|
|
1775
|
-
now: number;
|
|
1776
|
-
}): stdiobridge {
|
|
1777
|
-
return {
|
|
1778
|
-
...input.bridge,
|
|
1779
|
-
connected: true,
|
|
1780
|
-
received: input.bridge.received + (input.direction === "inbound" ? 1 : 0),
|
|
1781
|
-
sent: input.bridge.sent + (input.direction === "outbound" ? 1 : 0),
|
|
1782
|
-
lastframeat: input.now,
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
/** Restarts a dead client process on demand: the bridge reconnects under the new process id, counts the restart and keeps its relay counters. */
|
|
1787
|
-
export function restartbridge(input: { bridge: stdiobridge; pid: number; now: number }): stdiobridge {
|
|
1788
|
-
return {
|
|
1789
|
-
...input.bridge,
|
|
1790
|
-
connected: true,
|
|
1791
|
-
pid: input.pid,
|
|
1792
|
-
restarts: input.bridge.restarts + 1,
|
|
1793
|
-
startedat: input.now,
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
/** Emits one framed log line for local debugging: only the event, the time and scalar fields ride the line so params, results and payloads never leak into logs. */
|
|
1798
|
-
export function framedlog(event: string, at: number, fields?: Record<string, string | number | boolean>): string {
|
|
1799
|
-
return JSON.stringify({ at, event, ...(fields ?? {}) });
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
/** Builds one mcp tool call record for the audit trail and the recent calls view: the client, the tool, the origin and the outcome without any payload; the 1.1.56 family adds the call id, the idempotency key and the dry run, mock, batch and replay markers. */
|
|
1803
|
-
export function toolcallevent(input: {
|
|
1804
|
-
id: string;
|
|
1805
|
-
clientid: string;
|
|
1806
|
-
tool: string;
|
|
1807
|
-
origin: string;
|
|
1808
|
-
ok: boolean;
|
|
1809
|
-
now: number;
|
|
1810
|
-
code?: rpcerrorcode;
|
|
1811
|
-
callid?: string;
|
|
1812
|
-
idempotencykey?: string;
|
|
1813
|
-
dryrun?: boolean;
|
|
1814
|
-
mocked?: boolean;
|
|
1815
|
-
batchid?: string;
|
|
1816
|
-
replayed?: boolean;
|
|
1817
|
-
}): toolcallrecord {
|
|
1818
|
-
return {
|
|
1819
|
-
id: input.id,
|
|
1820
|
-
clientid: input.clientid,
|
|
1821
|
-
tool: input.tool,
|
|
1822
|
-
origin: input.origin,
|
|
1823
|
-
ok: input.ok,
|
|
1824
|
-
...(input.code !== undefined ? { code: input.code } : {}),
|
|
1825
|
-
at: input.now,
|
|
1826
|
-
...(input.callid !== undefined ? { callid: input.callid } : {}),
|
|
1827
|
-
...(input.idempotencykey !== undefined && input.idempotencykey.trim() !== ""
|
|
1828
|
-
? { idempotencykey: input.idempotencykey }
|
|
1829
|
-
: {}),
|
|
1830
|
-
...(input.dryrun === true ? { dryrun: true } : {}),
|
|
1831
|
-
...(input.mocked === true ? { mocked: true } : {}),
|
|
1832
|
-
...(input.batchid !== undefined ? { batchid: input.batchid } : {}),
|
|
1833
|
-
...(input.replayed === true ? { replayed: true } : {}),
|
|
1834
|
-
};
|
|
1835
|
-
}
|