@descryy/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/action-handshake.d.ts +86 -0
- package/dist/action-handshake.d.ts.map +1 -0
- package/dist/action-handshake.js +144 -0
- package/dist/action-handshake.js.map +1 -0
- package/dist/bin/descry-mcp.d.ts +18 -0
- package/dist/bin/descry-mcp.d.ts.map +1 -0
- package/dist/bin/descry-mcp.js +54 -0
- package/dist/bin/descry-mcp.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/language-guess.d.ts +17 -0
- package/dist/language-guess.d.ts.map +1 -0
- package/dist/language-guess.js +43 -0
- package/dist/language-guess.js.map +1 -0
- package/dist/protocol.d.ts +91 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +127 -0
- package/dist/protocol.js.map +1 -0
- package/dist/registry.d.ts +72 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +136 -0
- package/dist/registry.js.map +1 -0
- package/dist/render.d.ts +275 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +333 -0
- package/dist/render.js.map +1 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +263 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +164 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +302 -0
- package/dist/session.js.map +1 -0
- package/dist/tools/analyze.d.ts +124 -0
- package/dist/tools/analyze.d.ts.map +1 -0
- package/dist/tools/analyze.js +411 -0
- package/dist/tools/analyze.js.map +1 -0
- package/dist/tools/contracts.d.ts +105 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +141 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/cross-pr.d.ts +137 -0
- package/dist/tools/cross-pr.d.ts.map +1 -0
- package/dist/tools/cross-pr.js +216 -0
- package/dist/tools/cross-pr.js.map +1 -0
- package/dist/tools/git-diff.d.ts +29 -0
- package/dist/tools/git-diff.d.ts.map +1 -0
- package/dist/tools/git-diff.js +117 -0
- package/dist/tools/git-diff.js.map +1 -0
- package/dist/tools/git-history.d.ts +27 -0
- package/dist/tools/git-history.d.ts.map +1 -0
- package/dist/tools/git-history.js +91 -0
- package/dist/tools/git-history.js.map +1 -0
- package/dist/tools/history.d.ts +63 -0
- package/dist/tools/history.d.ts.map +1 -0
- package/dist/tools/history.js +114 -0
- package/dist/tools/history.js.map +1 -0
- package/dist/tools/impact.d.ts +81 -0
- package/dist/tools/impact.d.ts.map +1 -0
- package/dist/tools/impact.js +229 -0
- package/dist/tools/impact.js.map +1 -0
- package/dist/tools/index.d.ts +33 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +47 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/kit.d.ts +183 -0
- package/dist/tools/kit.d.ts.map +1 -0
- package/dist/tools/kit.js +117 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/link-workspace.d.ts +62 -0
- package/dist/tools/link-workspace.d.ts.map +1 -0
- package/dist/tools/link-workspace.js +156 -0
- package/dist/tools/link-workspace.js.map +1 -0
- package/dist/tools/lookup.d.ts +29 -0
- package/dist/tools/lookup.d.ts.map +1 -0
- package/dist/tools/lookup.js +73 -0
- package/dist/tools/lookup.js.map +1 -0
- package/dist/tools/pr-analysis.d.ts +89 -0
- package/dist/tools/pr-analysis.d.ts.map +1 -0
- package/dist/tools/pr-analysis.js +169 -0
- package/dist/tools/pr-analysis.js.map +1 -0
- package/dist/tools/propagation.d.ts +74 -0
- package/dist/tools/propagation.d.ts.map +1 -0
- package/dist/tools/propagation.js +194 -0
- package/dist/tools/propagation.js.map +1 -0
- package/dist/tools/questions.d.ts +80 -0
- package/dist/tools/questions.d.ts.map +1 -0
- package/dist/tools/questions.js +213 -0
- package/dist/tools/questions.js.map +1 -0
- package/dist/tools/refusal-fetch.d.ts +71 -0
- package/dist/tools/refusal-fetch.d.ts.map +1 -0
- package/dist/tools/refusal-fetch.js +137 -0
- package/dist/tools/refusal-fetch.js.map +1 -0
- package/dist/tools/scope.d.ts +60 -0
- package/dist/tools/scope.d.ts.map +1 -0
- package/dist/tools/scope.js +160 -0
- package/dist/tools/scope.js.map +1 -0
- package/dist/tools/similar-incidents.d.ts +105 -0
- package/dist/tools/similar-incidents.d.ts.map +1 -0
- package/dist/tools/similar-incidents.js +228 -0
- package/dist/tools/similar-incidents.js.map +1 -0
- package/dist/tools/validate.d.ts +103 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +176 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/verification-status.d.ts +68 -0
- package/dist/tools/verification-status.d.ts.map +1 -0
- package/dist/tools/verification-status.js +179 -0
- package/dist/tools/verification-status.js.map +1 -0
- package/dist/transport.d.ts +58 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +115 -0
- package/dist/transport.js.map +1 -0
- package/package.json +33 -0
package/dist/render.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way an engine answer becomes an MCP tool result.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this file exists rather than each tool formatting its own reply
|
|
5
|
+
*
|
|
6
|
+
* `QueryResult` carries three things a caller would rather not think about: a
|
|
7
|
+
* staleness stamp (§27.3), a `truncated` flag, and `notes` — the plain-words
|
|
8
|
+
* disclosures that populate the five result states' `refused`/`empty`/`failed`/
|
|
9
|
+
* `timed_out` cases. The engine computes all three correctly today. Every one of
|
|
10
|
+
* them is one careless `return result.data` away from never reaching a user.
|
|
11
|
+
*
|
|
12
|
+
* So the tool handler's return type is `QueryResult`, not content. A handler
|
|
13
|
+
* *cannot* build a reply; it can only produce an answer, and this module is the
|
|
14
|
+
* only thing that turns an answer into frames. Rule 7 — honest degradation, no
|
|
15
|
+
* silent doing-less — becomes a property of the type system rather than of
|
|
16
|
+
* whoever writes the next tool.
|
|
17
|
+
*
|
|
18
|
+
* ## `ai-tool-contract.md` §2 — the frozen envelope
|
|
19
|
+
*
|
|
20
|
+
* This module used to ship a two-value `status: "ok" | "not_analysable"`. The
|
|
21
|
+
* contract froze five: `ok` / `empty` / `refused` / `failed` / `timed_out`
|
|
22
|
+
* (§1), plus `class`, `toolVersion`, `budget` and a structured `truncated`
|
|
23
|
+
* object (§2). `AnswerStatus`/`renderAnswer`'s old two-state shape is gone
|
|
24
|
+
* rather than kept alongside the new one — two envelope shapes on the wire is
|
|
25
|
+
* the thing DEC-277's "one process mints" reasoning exists to avoid one layer
|
|
26
|
+
* up, applied here to a type rather than a decision number.
|
|
27
|
+
*
|
|
28
|
+
* ## Four invariants, asserted rather than documented (§2.1)
|
|
29
|
+
*
|
|
30
|
+
* `render.ts` already enforced the first two; the other two are new.
|
|
31
|
+
*
|
|
32
|
+
* 1. `truncated != null` with empty `disclosures` → throw.
|
|
33
|
+
* 2. `state` in `{refused, failed, timed_out}` with empty `disclosures` → throw.
|
|
34
|
+
* 3. `state == "refused"` with no verbatim reason in `disclosures` → throw (§4).
|
|
35
|
+
* 4. `class == "action"` returning anything other than a handshake envelope on
|
|
36
|
+
* the first call → throw (§7) — enforced in `action-handshake.ts` and
|
|
37
|
+
* `server.ts`, not here: this module never sees a mint, only a redeemed run.
|
|
38
|
+
*/
|
|
39
|
+
import { reliabilityCap, reliabilityNote, reportCategory as deriveReportCategory, } from "@descryy/ir";
|
|
40
|
+
/**
|
|
41
|
+
* MK-5 — which of the sixteen tools states a conclusion about the user's code
|
|
42
|
+
* versus which reports a fact about the graph.
|
|
43
|
+
*
|
|
44
|
+
* Lives here rather than in `@descryy/ir`: `tool-surface.ts`'s own header
|
|
45
|
+
* scopes that file to exactly `ResultState` and `ToolClass` ("and nothing
|
|
46
|
+
* else"), because those two cross into the pipeline and the desktop surface.
|
|
47
|
+
* This distinction is agent-mode-only (`plugin-and-mcp-surface.md` §3 PL-1),
|
|
48
|
+
* so a third shared union there would be scope creep the file's own doc
|
|
49
|
+
* disclaims. `kit.ts` imports it from here rather than the reverse, because
|
|
50
|
+
* `kit.ts` already depends on this module for `ToolAnswer`/`TruncationDetail`
|
|
51
|
+
* — defining it in `kit.ts` instead would make this module import back from
|
|
52
|
+
* `kit.ts` and cycle.
|
|
53
|
+
*
|
|
54
|
+
* - `conclusion` — the output is a statement about the user's code
|
|
55
|
+
* (`validate`, `contracts`, `cross_pr`). Never raw candidates: an
|
|
56
|
+
* already-adjudicated result carrying the category and the disclosure
|
|
57
|
+
* sentence as one payload.
|
|
58
|
+
* - `evidence` — the output is a fact the graph computed, already carrying
|
|
59
|
+
* its own resolution and qualification. The other thirteen.
|
|
60
|
+
*/
|
|
61
|
+
export const TOOL_TIERS = ["conclusion", "evidence"];
|
|
62
|
+
const PROVENANCE = {
|
|
63
|
+
surface: "mcp",
|
|
64
|
+
adjudicatedBy: "descry",
|
|
65
|
+
presentedBy: "host-agent",
|
|
66
|
+
governancePipeline: false,
|
|
67
|
+
};
|
|
68
|
+
export class DisclosureError extends Error {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "DisclosureError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* MK-8's derivation, shared by every envelope-producing function below so the
|
|
76
|
+
* rule lives in one place rather than four.
|
|
77
|
+
*/
|
|
78
|
+
function deriveEnvelopeCategory(tier, state, resolutionFloor, nameLevel) {
|
|
79
|
+
if (tier === "conclusion")
|
|
80
|
+
return null;
|
|
81
|
+
if (state === "failed" || state === "timed_out")
|
|
82
|
+
return null;
|
|
83
|
+
if (state === "refused")
|
|
84
|
+
return "not analysable";
|
|
85
|
+
return deriveReportCategory({
|
|
86
|
+
hasRuntimeEvidence: false,
|
|
87
|
+
directMechanismEvidence: false,
|
|
88
|
+
independentSignalTypes: 0,
|
|
89
|
+
// `renderAnswer`'s own `state`/`disclosures` invariants guarantee a real
|
|
90
|
+
// graph was consulted whenever state is ok/empty, so this is never the
|
|
91
|
+
// null-graph sentinel — cast is safe, not asserted blind.
|
|
92
|
+
resolution: (resolutionFloor ?? 0),
|
|
93
|
+
nameLevel,
|
|
94
|
+
humanAsserted: false,
|
|
95
|
+
}).category;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A refusal's reason must be the specific, verbatim thing that blocked it —
|
|
99
|
+
* never just the state name. This is a heuristic proxy for that rule (§4):
|
|
100
|
+
* `disclosures` must contain something a template did not produce, which we
|
|
101
|
+
* cannot verify from here, but we CAN verify the weaker, structurally
|
|
102
|
+
* checkable half — that at least one disclosure exists and it is not empty
|
|
103
|
+
* text — the same shape invariant 2 already asserts. The stronger claim (the
|
|
104
|
+
* reason is *this* refusal's actual cause, not a generic string) is a
|
|
105
|
+
* per-tool authoring discipline this function cannot see into; it is checked
|
|
106
|
+
* by `tool-contract-gate.mjs`'s exemplar reading instead.
|
|
107
|
+
*/
|
|
108
|
+
export function renderAnswer(tool, answer, options) {
|
|
109
|
+
const { result, nameLevel } = answer;
|
|
110
|
+
const state = answer.state ?? "ok";
|
|
111
|
+
if (result.truncated && result.notes.length === 0) {
|
|
112
|
+
throw new DisclosureError(`${tool} returned a truncated answer with no disclosure. A partial result that does not ` +
|
|
113
|
+
"say it is partial reads as a complete one, which is the failure rule 7 exists to " +
|
|
114
|
+
"prevent. Fix the query, not this check.");
|
|
115
|
+
}
|
|
116
|
+
if ((state === "refused" || state === "failed" || state === "timed_out") && result.notes.length === 0) {
|
|
117
|
+
throw new DisclosureError(`${tool} reported "${state}" without saying why. The five result states exist to distinguish ` +
|
|
118
|
+
'"we found nothing" from "we could not look" from "something broke"; with no reason attached ' +
|
|
119
|
+
"this reply says none of them.");
|
|
120
|
+
}
|
|
121
|
+
const resolutionFloor = result.stamp.resolutionFloor;
|
|
122
|
+
const cap = reliabilityCap(resolutionFloor, nameLevel);
|
|
123
|
+
const disclosures = [...result.notes, reliabilityNote(resolutionFloor, nameLevel)];
|
|
124
|
+
const envelope = {
|
|
125
|
+
tool,
|
|
126
|
+
toolVersion: options.toolVersion,
|
|
127
|
+
state,
|
|
128
|
+
class: options.toolClass,
|
|
129
|
+
headline: answer.headline,
|
|
130
|
+
data: result.data,
|
|
131
|
+
graph: {
|
|
132
|
+
commitSha: result.stamp.commitSha,
|
|
133
|
+
builtAt: result.stamp.graphBuiltAt === null
|
|
134
|
+
? null
|
|
135
|
+
: new Date(result.stamp.graphBuiltAt).toISOString(),
|
|
136
|
+
irSchemaVersion: result.stamp.irSchemaVersion,
|
|
137
|
+
resolutionFloor,
|
|
138
|
+
reliabilityCap: cap,
|
|
139
|
+
},
|
|
140
|
+
truncated: !result.truncated
|
|
141
|
+
? null
|
|
142
|
+
: (answer.truncatedDetail ?? { shown: -1, total: -1, more: null }),
|
|
143
|
+
refusals: answer.refusalSummary ?? null,
|
|
144
|
+
budget: { elapsedMs: options.elapsedMs, limitMs: options.limitMs },
|
|
145
|
+
disclosures,
|
|
146
|
+
reportCategory: deriveEnvelopeCategory(options.toolTier, state, resolutionFloor, nameLevel),
|
|
147
|
+
provenance: PROVENANCE,
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: narrate(envelope) }],
|
|
151
|
+
structuredContent: envelope,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The text an agent actually reads.
|
|
156
|
+
*
|
|
157
|
+
* Ordered so that the disclosures are **last**, because that is what a model
|
|
158
|
+
* summarising the reply keeps. A caveat buried above the data is a caveat that
|
|
159
|
+
* gets dropped in the summary the developer sees.
|
|
160
|
+
*/
|
|
161
|
+
function narrate(envelope) {
|
|
162
|
+
const lines = [envelope.headline];
|
|
163
|
+
if (envelope.state !== "ok" && envelope.state !== "empty") {
|
|
164
|
+
lines.push("", `**${envelope.state}.** ` +
|
|
165
|
+
(envelope.state === "refused"
|
|
166
|
+
? "This is not a result — see below for why Descry could not look."
|
|
167
|
+
: envelope.state === "failed"
|
|
168
|
+
? "Something broke while answering this call — see below."
|
|
169
|
+
: "The call ran out of its declared time budget — see below."));
|
|
170
|
+
}
|
|
171
|
+
else if (envelope.state === "empty") {
|
|
172
|
+
lines.push("", "**Empty.** Descry looked completely and there was nothing to find — see below.");
|
|
173
|
+
}
|
|
174
|
+
const stale = envelope.graph.commitSha === null
|
|
175
|
+
? "The graph is empty — nothing has been analysed into it yet."
|
|
176
|
+
: `Graph at ${envelope.graph.commitSha.slice(0, 12)}` +
|
|
177
|
+
(envelope.graph.builtAt === null ? "" : `, built ${envelope.graph.builtAt}`) +
|
|
178
|
+
`. Weakest evidence R${envelope.graph.resolutionFloor ?? "n/a"}; findings here cap at ` +
|
|
179
|
+
`reliability class ${envelope.graph.reliabilityCap}.`;
|
|
180
|
+
lines.push("", stale);
|
|
181
|
+
if (envelope.disclosures.length > 0) {
|
|
182
|
+
lines.push("", "What this answer does not cover:");
|
|
183
|
+
for (const note of envelope.disclosures)
|
|
184
|
+
lines.push(`- ${note}`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A failure that is the caller's to fix — a bad argument, a missing graph.
|
|
190
|
+
*
|
|
191
|
+
* Returned as a tool result with `isError`, not as a JSON-RPC error, which is
|
|
192
|
+
* what the MCP spec asks for: protocol errors are for malformed protocol, tool
|
|
193
|
+
* errors are for tools, and an agent can act on the second only if it arrives as
|
|
194
|
+
* content it can read.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately outside the five-state envelope: a bad argument is not a
|
|
197
|
+
* statement about what Descry found or could not find, it is "fix your call
|
|
198
|
+
* and retry" — a different kind of thing than `refused`/`failed`.
|
|
199
|
+
*/
|
|
200
|
+
export function renderToolError(tool, message) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: `${tool}: ${message}` }],
|
|
203
|
+
structuredContent: { tool, status: "error", message },
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `state: "failed"` — §1.1: "promoted from a transport error to a result."
|
|
209
|
+
*
|
|
210
|
+
* An unexpected/internal error, not a bad argument (`renderToolError` still
|
|
211
|
+
* handles those). `data` is always `null` here (§2.2): the tool does not know
|
|
212
|
+
* what happened, and anything it returned would be a guess about its own
|
|
213
|
+
* fault. `isError` stays `true` so a generic MCP client that only reads the
|
|
214
|
+
* transport-level signal still treats this as a failure; an AI-layer-aware
|
|
215
|
+
* caller additionally gets `state: "failed"` in `structuredContent` to reason
|
|
216
|
+
* about rather than a bare `{tool, status, message}` shape.
|
|
217
|
+
*/
|
|
218
|
+
export function renderFailed(tool, message, options) {
|
|
219
|
+
const envelope = {
|
|
220
|
+
tool,
|
|
221
|
+
toolVersion: options.toolVersion,
|
|
222
|
+
state: "failed",
|
|
223
|
+
class: options.toolClass,
|
|
224
|
+
headline: `${tool} failed.`,
|
|
225
|
+
data: null,
|
|
226
|
+
graph: {
|
|
227
|
+
commitSha: null,
|
|
228
|
+
builtAt: null,
|
|
229
|
+
irSchemaVersion: 0,
|
|
230
|
+
resolutionFloor: null,
|
|
231
|
+
reliabilityCap: "C",
|
|
232
|
+
},
|
|
233
|
+
truncated: null,
|
|
234
|
+
refusals: null,
|
|
235
|
+
budget: { elapsedMs: options.elapsedMs, limitMs: options.limitMs },
|
|
236
|
+
disclosures: [message],
|
|
237
|
+
// A fault is not a claim about the code either way (§20) — no category applies.
|
|
238
|
+
reportCategory: null,
|
|
239
|
+
provenance: PROVENANCE,
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: narrate(envelope) }],
|
|
243
|
+
structuredContent: envelope,
|
|
244
|
+
isError: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The one legitimate producer of a pending-handshake payload — §7's mint call.
|
|
249
|
+
*
|
|
250
|
+
* `class == "action"` returning anything other than this shape on an
|
|
251
|
+
* unconfirmed first call is invariant 4 (§2.1): "throw." This function is
|
|
252
|
+
* where that shape is produced, so the throw lives at the one call site that
|
|
253
|
+
* builds anything else for an action tool's first call — `server.ts`'s
|
|
254
|
+
* dispatch loop calls this and only this when `resolveActionDispatch` returns
|
|
255
|
+
* `{kind: "handshake"}`, never `tool.run`.
|
|
256
|
+
*
|
|
257
|
+
* `state: "ok"` deliberately: a mint is not a refusal, a failure or an empty
|
|
258
|
+
* result — it is a successful "here is what I would do", and the *pending*
|
|
259
|
+
* flag inside `data` is what tells a caller nothing was performed yet.
|
|
260
|
+
*/
|
|
261
|
+
export function renderHandshake(tool, token, willDo, options) {
|
|
262
|
+
const envelope = {
|
|
263
|
+
tool,
|
|
264
|
+
toolVersion: options.toolVersion,
|
|
265
|
+
state: "ok",
|
|
266
|
+
class: options.toolClass,
|
|
267
|
+
headline: `Confirmation required: ${willDo}`,
|
|
268
|
+
data: { pending: true, token, willDo },
|
|
269
|
+
graph: {
|
|
270
|
+
commitSha: null,
|
|
271
|
+
builtAt: null,
|
|
272
|
+
irSchemaVersion: 0,
|
|
273
|
+
resolutionFloor: null,
|
|
274
|
+
reliabilityCap: "C",
|
|
275
|
+
},
|
|
276
|
+
truncated: null,
|
|
277
|
+
refusals: null,
|
|
278
|
+
budget: { elapsedMs: options.elapsedMs, limitMs: options.limitMs },
|
|
279
|
+
disclosures: [
|
|
280
|
+
`Nothing was performed. Call ${tool} again with { "confirmToken": "${token}" } to actually do this — ` +
|
|
281
|
+
"the token expires in five minutes and is single-use.",
|
|
282
|
+
],
|
|
283
|
+
// Nothing was analysed yet — a confirmation prompt makes no claim to categorize.
|
|
284
|
+
reportCategory: null,
|
|
285
|
+
provenance: PROVENANCE,
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: narrate(envelope) }],
|
|
289
|
+
structuredContent: envelope,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* `state: "timed_out"` — the call ran out of its declared budget.
|
|
294
|
+
*
|
|
295
|
+
* §6: "not a silent truncation and never a `failed`". `data` is `null` here
|
|
296
|
+
* because none of the registered tools currently produce genuine partial
|
|
297
|
+
* results mid-run (none checks `ctx.signal` internally yet — a disclosed gap,
|
|
298
|
+
* not a silent one, named in the returned disclosure); the envelope shape
|
|
299
|
+
* supports a populated partial `data` the day a tool does.
|
|
300
|
+
*/
|
|
301
|
+
export function renderTimedOut(tool, options) {
|
|
302
|
+
const envelope = {
|
|
303
|
+
tool,
|
|
304
|
+
toolVersion: options.toolVersion,
|
|
305
|
+
state: "timed_out",
|
|
306
|
+
class: options.toolClass,
|
|
307
|
+
headline: `${tool} timed out.`,
|
|
308
|
+
data: null,
|
|
309
|
+
graph: {
|
|
310
|
+
commitSha: null,
|
|
311
|
+
builtAt: null,
|
|
312
|
+
irSchemaVersion: 0,
|
|
313
|
+
resolutionFloor: null,
|
|
314
|
+
reliabilityCap: "C",
|
|
315
|
+
},
|
|
316
|
+
truncated: null,
|
|
317
|
+
refusals: null,
|
|
318
|
+
budget: { elapsedMs: options.elapsedMs, limitMs: options.limitMs },
|
|
319
|
+
disclosures: [
|
|
320
|
+
`${tool} did not finish within its ${options.limitMs}ms budget. This is a partial answer, not ` +
|
|
321
|
+
"a finding of failure or of absence — retry, or narrow the request.",
|
|
322
|
+
],
|
|
323
|
+
// A partial run in progress makes no claim to categorize yet.
|
|
324
|
+
reportCategory: null,
|
|
325
|
+
provenance: PROVENANCE,
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: "text", text: narrate(envelope) }],
|
|
329
|
+
structuredContent: envelope,
|
|
330
|
+
isError: true,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server: handshake, tool listing, tool calls, streaming narration.
|
|
3
|
+
*
|
|
4
|
+
* ## The layering that makes this testable without a pipe
|
|
5
|
+
*
|
|
6
|
+
* `dispatch` takes a line of text and a `send`, and that is the whole surface.
|
|
7
|
+
* The stdio transport is a loop around it; a test is a call to it. So the
|
|
8
|
+
* handshake, the error frames and the progress notifications are all covered by
|
|
9
|
+
* ordinary unit tests rather than by spawning a process and hoping.
|
|
10
|
+
*
|
|
11
|
+
* ## Streaming narration — §22.2's primary UX, at the protocol level
|
|
12
|
+
*
|
|
13
|
+
* A developer asking a question in their editor and watching the work happen is
|
|
14
|
+
* the most-used delivery path in the architecture. MCP's mechanism is
|
|
15
|
+
* `notifications/progress`, sent only when the request carried a
|
|
16
|
+
* `_meta.progressToken`. Tools call `ctx.progress(...)` unconditionally and this
|
|
17
|
+
* layer decides whether anyone is listening — the alternative, tools checking
|
|
18
|
+
* first, is how narration ends up in some code paths and not others.
|
|
19
|
+
*
|
|
20
|
+
* ## Every tool failure is a *tool* result, not a protocol error
|
|
21
|
+
*
|
|
22
|
+
* The spec is explicit and the reason is practical: a JSON-RPC error is for a
|
|
23
|
+
* malformed request, and an agent that receives one has nothing to reason about.
|
|
24
|
+
* A bad argument, a missing graph, an unreadable repository — those come back as
|
|
25
|
+
* content with `isError`, which the model can read and act on. Only genuinely
|
|
26
|
+
* broken protocol produces an error frame.
|
|
27
|
+
*/
|
|
28
|
+
import type { Session } from "./session.ts";
|
|
29
|
+
import type { Transport } from "./transport.ts";
|
|
30
|
+
export interface McpServerOptions {
|
|
31
|
+
readonly session: Session;
|
|
32
|
+
readonly name?: string;
|
|
33
|
+
readonly version?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface McpServer {
|
|
36
|
+
/** Handle one newline-delimited frame. Never throws; every failure becomes a frame. */
|
|
37
|
+
dispatch(line: string, transport: Transport): Promise<void>;
|
|
38
|
+
/** True once the client has completed the handshake. */
|
|
39
|
+
readonly initialized: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function createMcpServer(options: McpServerOptions): McpServer;
|
|
42
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAwBH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAYhD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,wDAAwD;IACxD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAyDD,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CA2MpE"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server: handshake, tool listing, tool calls, streaming narration.
|
|
3
|
+
*
|
|
4
|
+
* ## The layering that makes this testable without a pipe
|
|
5
|
+
*
|
|
6
|
+
* `dispatch` takes a line of text and a `send`, and that is the whole surface.
|
|
7
|
+
* The stdio transport is a loop around it; a test is a call to it. So the
|
|
8
|
+
* handshake, the error frames and the progress notifications are all covered by
|
|
9
|
+
* ordinary unit tests rather than by spawning a process and hoping.
|
|
10
|
+
*
|
|
11
|
+
* ## Streaming narration — §22.2's primary UX, at the protocol level
|
|
12
|
+
*
|
|
13
|
+
* A developer asking a question in their editor and watching the work happen is
|
|
14
|
+
* the most-used delivery path in the architecture. MCP's mechanism is
|
|
15
|
+
* `notifications/progress`, sent only when the request carried a
|
|
16
|
+
* `_meta.progressToken`. Tools call `ctx.progress(...)` unconditionally and this
|
|
17
|
+
* layer decides whether anyone is listening — the alternative, tools checking
|
|
18
|
+
* first, is how narration ends up in some code paths and not others.
|
|
19
|
+
*
|
|
20
|
+
* ## Every tool failure is a *tool* result, not a protocol error
|
|
21
|
+
*
|
|
22
|
+
* The spec is explicit and the reason is practical: a JSON-RPC error is for a
|
|
23
|
+
* malformed request, and an agent that receives one has nothing to reason about.
|
|
24
|
+
* A bad argument, a missing graph, an unreadable repository — those come back as
|
|
25
|
+
* content with `isError`, which the model can read and act on. Only genuinely
|
|
26
|
+
* broken protocol produces an error frame.
|
|
27
|
+
*/
|
|
28
|
+
import { resolveActionDispatch } from "./action-handshake.js";
|
|
29
|
+
import { fail, isRequest, negotiateVersion, ok, parseMessage, ProtocolError, RPC_INTERNAL_ERROR, RPC_INVALID_PARAMS, RPC_METHOD_NOT_FOUND, } from "./protocol.js";
|
|
30
|
+
import { renderAnswer, renderFailed, renderHandshake, renderTimedOut, renderToolError, } from "./render.js";
|
|
31
|
+
import { TOOLS, TOOLS_BY_NAME, ToolInputError } from "./tools/index.js";
|
|
32
|
+
/** Every call's declared budget (§6). Fixed today; per-trigger budgets are P26/P28 work. */
|
|
33
|
+
const CALL_BUDGET_MS = 15 * 60 * 1000;
|
|
34
|
+
function isAbort(error) {
|
|
35
|
+
return ((error instanceof Error && error.name === "AbortError") ||
|
|
36
|
+
(typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "TimeoutError"));
|
|
37
|
+
}
|
|
38
|
+
const SERVER_INSTRUCTIONS = "Descry reads the repository's source into a code graph and answers questions about it.\n\n" +
|
|
39
|
+
"Call `analyze` first — every other tool queries the graph it builds, and a stale graph " +
|
|
40
|
+
"answers about code that has changed.\n\n" +
|
|
41
|
+
"Every reply has a `state`, one of five, and they are different claims: `ok` (found " +
|
|
42
|
+
"something), `empty` (looked completely, found nothing — a fact about your code), `refused` " +
|
|
43
|
+
"(could not look — a fact about Descry, never about your code), `failed` (something broke) " +
|
|
44
|
+
"and `timed_out` (ran out of its time budget, partial answer). A tool that answered `[]` for " +
|
|
45
|
+
"both \"nothing matched\" and \"I could not look\" would teach a false fact with no way to " +
|
|
46
|
+
"detect it — that is why these are distinct. Never read `refused` as \"clean\" or `empty` as " +
|
|
47
|
+
"\"broken\".\n\n" +
|
|
48
|
+
"Every reply also carries a `disclosures` list, a `resolutionFloor` and a `reliabilityCap` " +
|
|
49
|
+
"(A, B or C) inside `graph`. `resolutionFloor` is how much tooling actually ran under the " +
|
|
50
|
+
"weakest evidence behind the answer, R0 through R4: R0 is syntax only, nothing resolved; R1 " +
|
|
51
|
+
"adds module/import resolution; R2 adds cross-file reference resolution (which symbol a name " +
|
|
52
|
+
"refers to, but no type checker); R3 adds a type checker, so claims about shapes are provable; " +
|
|
53
|
+
"R4 means the fact was observed at runtime rather than inferred. `reliabilityCap` is what that " +
|
|
54
|
+
"floor permits the claim to be relayed as: C means the claim is a behavioural prediction that " +
|
|
55
|
+
"has not been confirmed by running anything, and must not be relayed as fact. B means the " +
|
|
56
|
+
"claim rests on structural or reference resolution — a name, an import, a call target — real " +
|
|
57
|
+
"evidence, but not yet a confirmed behaviour. A means the claim is as strong as this graph can " +
|
|
58
|
+
"make it: a name-level fact resolved at R2 or better, a shape-level fact backed by a type " +
|
|
59
|
+
"checker (R3), or anything witnessed at runtime (R4, unconditional). Read the disclosures " +
|
|
60
|
+
"before summarising — they say what the answer does *not* cover.\n\n" +
|
|
61
|
+
"Coverage is a separate question from confidence: call `verification_status` with a list of " +
|
|
62
|
+
"node ids or file paths from an earlier reply to learn which of them the graph actually " +
|
|
63
|
+
"reached, and at what resolution, rather than assuming silence means untested. `refusal_fetch` " +
|
|
64
|
+
"gives the graph's full ledger of things an adapter could not resolve — call it with no " +
|
|
65
|
+
"arguments for a one-line summary and a handle, then again with that handle to page through " +
|
|
66
|
+
"every row; never assume the summary's one exemplar is the only refusal.\n\n" +
|
|
67
|
+
"`class` on each tool (`read`, `stateful-read` or `action`) says whether it is safe to retry. " +
|
|
68
|
+
"The one `action`-class tool (`questions`, when answering) takes a two-call handshake: the " +
|
|
69
|
+
"first call performs nothing and returns a confirmation token; only a second call presenting " +
|
|
70
|
+
"that exact token performs the write.\n\n" +
|
|
71
|
+
"`tier` on each tool is `conclusion` for the three that already state a verdict about your " +
|
|
72
|
+
"code (`validate`, `contracts`, `cross_pr` — quote their `headline` verbatim rather than " +
|
|
73
|
+
"rephrasing it) or `evidence` for the other thirteen, which return graph facts for you to " +
|
|
74
|
+
"reason over yourself. Every reply also carries `reportCategory` — one of `confirmed`, " +
|
|
75
|
+
"`strongly supported`, `unconfirmed`, `not analysable`, `unsupported`, or `null` where no " +
|
|
76
|
+
"claim applies. It is `null` on every `conclusion`-tier reply (the `headline` already states " +
|
|
77
|
+
"the verdict in plain words) and, on an `evidence`-tier reply, `unconfirmed` at best: this " +
|
|
78
|
+
"server has no runtime evidence to offer, so `confirmed` never appears here — that word is " +
|
|
79
|
+
"reserved for the full Descry app, and its absence is honest, not a bug. `provenance` names " +
|
|
80
|
+
"this surface explicitly (`adjudicatedBy: \"descry\"`, `presentedBy: \"host-agent\"`) so a " +
|
|
81
|
+
"reply from here is never mistaken for one the app itself adjudicated and presented.\n\n" +
|
|
82
|
+
"In agent mode, Descry gives you high-reliability facts about your code. It does not give you " +
|
|
83
|
+
"confirmed behaviour, because it did not run your application — `descry-runtime`'s live loop " +
|
|
84
|
+
"does not execute inside an MCP call, at any resolution level or however careful the static " +
|
|
85
|
+
"analysis was. `confirmed` and `strongly supported` are the desktop app's differentiator, not " +
|
|
86
|
+
"this surface's; treat every reply here as a strong lead to verify, never as a closed case.\n\n" +
|
|
87
|
+
"Descry cannot enforce any of this once you leave this server: nothing stops you from adding " +
|
|
88
|
+
"your own claims on top of what a tool returned (the fact boundary), and nothing stops you " +
|
|
89
|
+
"from investigating paths no tool surfaced or suggested (the candidate boundary) — this text " +
|
|
90
|
+
"cannot make you not do either. The fields above exist so you do not have to guess.";
|
|
91
|
+
export function createMcpServer(options) {
|
|
92
|
+
const session = options.session;
|
|
93
|
+
const serverName = options.name ?? "descry";
|
|
94
|
+
const serverVersion = options.version ?? "0.0.0";
|
|
95
|
+
let initialized = false;
|
|
96
|
+
async function handleRequest(request, transport) {
|
|
97
|
+
const { id, method } = request;
|
|
98
|
+
const params = request.params ?? {};
|
|
99
|
+
switch (method) {
|
|
100
|
+
case "initialize": {
|
|
101
|
+
transport.send(ok(id, {
|
|
102
|
+
protocolVersion: negotiateVersion(params["protocolVersion"]),
|
|
103
|
+
// `listChanged: false` is honest: the tool set is fixed at startup,
|
|
104
|
+
// so the server will never send a list-changed notification and
|
|
105
|
+
// should not claim it might.
|
|
106
|
+
capabilities: { tools: { listChanged: false } },
|
|
107
|
+
serverInfo: { name: serverName, version: serverVersion },
|
|
108
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
109
|
+
}));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
case "ping": {
|
|
113
|
+
transport.send(ok(id, {}));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
case "tools/list": {
|
|
117
|
+
transport.send(ok(id, {
|
|
118
|
+
tools: TOOLS.map((tool) => ({
|
|
119
|
+
name: tool.name,
|
|
120
|
+
title: tool.title,
|
|
121
|
+
description: tool.description,
|
|
122
|
+
inputSchema: tool.inputSchema,
|
|
123
|
+
// ai-tool-contract.md §8: every endpoint declares a class, and an
|
|
124
|
+
// external client has no other way to read it — §6's retry table
|
|
125
|
+
// (read=retryable, stateful-read=idempotent-retryable,
|
|
126
|
+
// action=no-retry+handshake-required) is meaningless without this
|
|
127
|
+
// on the wire.
|
|
128
|
+
class: tool.class,
|
|
129
|
+
// MK-5: whether this tool's output is a conclusion about the
|
|
130
|
+
// user's code or a fact about the graph — same "no other way to
|
|
131
|
+
// read it" reasoning as `class`, one line up.
|
|
132
|
+
tier: tool.tier,
|
|
133
|
+
toolVersion: tool.version,
|
|
134
|
+
})),
|
|
135
|
+
}));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case "tools/call": {
|
|
139
|
+
transport.send(ok(id, await callTool(id, params, transport)));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
transport.send(fail(id, RPC_METHOD_NOT_FOUND, `unknown method: ${method}`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function callTool(id, params, transport) {
|
|
147
|
+
const name = params["name"];
|
|
148
|
+
if (typeof name !== "string") {
|
|
149
|
+
return renderToolError("tools/call", '"name" must be a string');
|
|
150
|
+
}
|
|
151
|
+
const tool = TOOLS_BY_NAME.get(name);
|
|
152
|
+
if (tool === undefined) {
|
|
153
|
+
return renderToolError(name, `no such tool. Available: ${TOOLS.map((t) => t.name).join(", ")}.`);
|
|
154
|
+
}
|
|
155
|
+
const rawArgs = params["arguments"];
|
|
156
|
+
if (rawArgs !== undefined && (typeof rawArgs !== "object" || rawArgs === null || Array.isArray(rawArgs))) {
|
|
157
|
+
return renderToolError(name, '"arguments" must be an object');
|
|
158
|
+
}
|
|
159
|
+
const args = (rawArgs ?? {});
|
|
160
|
+
const meta = params["_meta"];
|
|
161
|
+
const token = typeof meta === "object" && meta !== null
|
|
162
|
+
? meta["progressToken"]
|
|
163
|
+
: undefined;
|
|
164
|
+
const progress = typeof token === "string" || typeof token === "number"
|
|
165
|
+
? (message, current, total) => {
|
|
166
|
+
transport.send({
|
|
167
|
+
jsonrpc: "2.0",
|
|
168
|
+
method: "notifications/progress",
|
|
169
|
+
params: {
|
|
170
|
+
progressToken: token,
|
|
171
|
+
// `progress` is required by the spec and must increase. When a
|
|
172
|
+
// tool narrates without a count, the message is the payload and
|
|
173
|
+
// the number is a monotonic tick rather than a fake percentage.
|
|
174
|
+
progress: current ?? nextTick(id),
|
|
175
|
+
...(total === undefined ? {} : { total }),
|
|
176
|
+
message,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
: () => { };
|
|
181
|
+
const ctx = {
|
|
182
|
+
session,
|
|
183
|
+
progress,
|
|
184
|
+
signal: AbortSignal.timeout(CALL_BUDGET_MS),
|
|
185
|
+
};
|
|
186
|
+
const started = Date.now();
|
|
187
|
+
const renderOptions = () => ({
|
|
188
|
+
toolVersion: tool.version,
|
|
189
|
+
toolClass: tool.class,
|
|
190
|
+
toolTier: tool.tier,
|
|
191
|
+
elapsedMs: Date.now() - started,
|
|
192
|
+
limitMs: CALL_BUDGET_MS,
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
// §7's two-call handshake. Decided before any real work starts — a mint
|
|
196
|
+
// has no I/O to time out on — so this sits outside the try/catch's
|
|
197
|
+
// failure/timeout handling below, which is for `tool.run` alone.
|
|
198
|
+
const dispatch = await resolveActionDispatch(tool, args, ctx);
|
|
199
|
+
if (dispatch.kind === "handshake") {
|
|
200
|
+
return renderHandshake(name, dispatch.token, dispatch.willDo, renderOptions());
|
|
201
|
+
}
|
|
202
|
+
const outcome = await tool.run(dispatch.args, ctx);
|
|
203
|
+
return renderAnswer(name, outcome, renderOptions());
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof ToolInputError)
|
|
207
|
+
return renderToolError(name, error.message);
|
|
208
|
+
if (isAbort(error) || ctx.signal.aborted)
|
|
209
|
+
return renderTimedOut(name, renderOptions());
|
|
210
|
+
// Anything else is a bug or an environment failure. It still comes back as
|
|
211
|
+
// a tool result: an agent can retry or tell the user, whereas a protocol
|
|
212
|
+
// error just ends the call with nothing it can use.
|
|
213
|
+
return renderFailed(name, `${error instanceof Error ? error.message : String(error)}`, renderOptions());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Monotonic per-request tick for narration with no natural count.
|
|
217
|
+
const ticks = new Map();
|
|
218
|
+
function nextTick(id) {
|
|
219
|
+
const next = (ticks.get(id) ?? 0) + 1;
|
|
220
|
+
ticks.set(id, next);
|
|
221
|
+
return next;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
get initialized() {
|
|
225
|
+
return initialized;
|
|
226
|
+
},
|
|
227
|
+
async dispatch(line, transport) {
|
|
228
|
+
let message;
|
|
229
|
+
try {
|
|
230
|
+
message = parseMessage(line);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error instanceof ProtocolError) {
|
|
234
|
+
// No usable id means no response is possible: JSON-RPC has nowhere to
|
|
235
|
+
// put the answer, and inventing an id would put a frame on the wire
|
|
236
|
+
// the client cannot correlate to anything it sent.
|
|
237
|
+
if (error.id !== null)
|
|
238
|
+
transport.send(fail(error.id, error.code, error.message));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
if (!isRequest(message)) {
|
|
244
|
+
if (message.method === "notifications/initialized")
|
|
245
|
+
initialized = true;
|
|
246
|
+
// Every other notification — cancellation, roots changed — is accepted
|
|
247
|
+
// and ignored. A notification must never be answered, so silence here is
|
|
248
|
+
// the protocol, not a gap.
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await handleRequest(message, transport);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
transport.send(fail(message.id, error instanceof ToolInputError ? RPC_INVALID_PARAMS : RPC_INTERNAL_ERROR, error instanceof Error ? error.message : String(error)));
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
ticks.delete(message.id);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=server.js.map
|