@gmickel/gno 1.30.7 → 1.31.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/README.md +2 -2
- package/assets/skill/SKILL.md +2 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.31.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/mcp.md +62 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +19 -0
- package/src/mcp/tools/sections.ts +512 -0
- package/src/sdk/client.ts +71 -1
- package/src/sdk/index.ts +5 -0
- package/src/sdk/types.ts +29 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +219 -36
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_section — read-only section target create/resolve.
|
|
3
|
+
*
|
|
4
|
+
* Always registered. No writes, no target persistence, no parser fork.
|
|
5
|
+
* Consumes shared core create/resolve + transport projection (fn-61.6).
|
|
6
|
+
*
|
|
7
|
+
* @module src/mcp/tools/sections
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import type { DocumentRow } from "../../store/types";
|
|
13
|
+
import type { ToolContext } from "../server";
|
|
14
|
+
|
|
15
|
+
import { MCP_ERRORS } from "../../core/errors";
|
|
16
|
+
import { parseRef } from "../../core/ref-parser";
|
|
17
|
+
import {
|
|
18
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
19
|
+
createSectionTarget,
|
|
20
|
+
extractSections,
|
|
21
|
+
isBoundedSectionTarget,
|
|
22
|
+
isTransportBoundedCanonicalUri,
|
|
23
|
+
parseSectionTargetCreateSelector,
|
|
24
|
+
parseSectionTargetV1,
|
|
25
|
+
projectSectionTargetCreateResult,
|
|
26
|
+
projectSectionTargetResolveResult,
|
|
27
|
+
resolveSectionTarget,
|
|
28
|
+
SECTION_TARGET_BOUNDS,
|
|
29
|
+
type SectionCitationV1,
|
|
30
|
+
type SectionResolutionDiagnostics,
|
|
31
|
+
type SectionResolutionStatus,
|
|
32
|
+
type SectionTargetV1,
|
|
33
|
+
} from "../../core/sections";
|
|
34
|
+
import { runTool, type ToolResult } from "./index";
|
|
35
|
+
|
|
36
|
+
export const SECTION_MCP_SCHEMA_VERSION = "1.0" as const;
|
|
37
|
+
|
|
38
|
+
const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
|
|
39
|
+
const REF_MAX_CHARS = 2048;
|
|
40
|
+
|
|
41
|
+
const positiveSafeInt = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
|
42
|
+
|
|
43
|
+
const boundedNonEmpty = (max: number) => z.string().min(1).max(max);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Closed SectionTargetV1 Zod mirror for MCP SDK validation.
|
|
47
|
+
* Object (not union) so McpServer can publish/normalize JSON Schema.
|
|
48
|
+
* Field bounds stay JSON-schema-compatible; cross-field rules via superRefine.
|
|
49
|
+
*/
|
|
50
|
+
export const sectionTargetInputSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
schemaVersion: z.literal("1"),
|
|
53
|
+
document: z
|
|
54
|
+
.object({
|
|
55
|
+
uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
|
|
59
|
+
headingPath: z
|
|
60
|
+
.array(boundedNonEmpty(SECTION_TARGET_BOUNDS.headingPathItemMaxChars))
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(SECTION_TARGET_BOUNDS.headingPathMaxItems),
|
|
63
|
+
occurrence: positiveSafeInt,
|
|
64
|
+
quote: z
|
|
65
|
+
.object({
|
|
66
|
+
exact: z.string().max(SECTION_TARGET_BOUNDS.exactMaxChars),
|
|
67
|
+
prefix: z.string().max(SECTION_TARGET_BOUNDS.prefixMaxChars),
|
|
68
|
+
suffix: z.string().max(SECTION_TARGET_BOUNDS.suffixMaxChars),
|
|
69
|
+
})
|
|
70
|
+
.strict(),
|
|
71
|
+
sourceFingerprint: z.string().regex(FINGERPRINT_PATTERN),
|
|
72
|
+
hints: z
|
|
73
|
+
.object({
|
|
74
|
+
line: positiveSafeInt,
|
|
75
|
+
startOffset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
76
|
+
endOffset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
77
|
+
})
|
|
78
|
+
.strict(),
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.superRefine((target, ctx) => {
|
|
82
|
+
if (target.hints.endOffset < target.hints.startOffset) {
|
|
83
|
+
ctx.addIssue({
|
|
84
|
+
code: z.ZodIssueCode.custom,
|
|
85
|
+
path: ["hints", "endOffset"],
|
|
86
|
+
message: "hints.endOffset must be >= hints.startOffset",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!isBoundedSectionTarget(target as SectionTargetV1)) {
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: z.ZodIssueCode.custom,
|
|
92
|
+
message: "Section target exceeds size bounds",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const refSchema = boundedNonEmpty(REF_MAX_CHARS).describe(
|
|
98
|
+
"Document reference: gno:// URI, collection/path, or #docid"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Closed create/resolve input. Single object schema (MCP SDK cannot list/validate
|
|
103
|
+
* Zod unions as tool inputSchema/outputSchema).
|
|
104
|
+
*/
|
|
105
|
+
export const sectionInputSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
action: z.enum(["create", "resolve"]),
|
|
108
|
+
ref: refSchema,
|
|
109
|
+
anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars).optional(),
|
|
110
|
+
line: positiveSafeInt.optional(),
|
|
111
|
+
target: sectionTargetInputSchema.optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict()
|
|
114
|
+
.superRefine((value, ctx) => {
|
|
115
|
+
if (value.action === "create") {
|
|
116
|
+
const hasAnchor = value.anchor !== undefined;
|
|
117
|
+
const hasLine = value.line !== undefined;
|
|
118
|
+
if (hasAnchor === hasLine) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: z.ZodIssueCode.custom,
|
|
121
|
+
message: "create requires exactly one of anchor|line",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (value.target !== undefined) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: z.ZodIssueCode.custom,
|
|
127
|
+
path: ["target"],
|
|
128
|
+
message: "create forbids target",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (value.target === undefined) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
path: ["target"],
|
|
137
|
+
message: "resolve requires target",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (value.anchor !== undefined || value.line !== undefined) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: z.ZodIssueCode.custom,
|
|
143
|
+
message: "resolve forbids anchor|line",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export type SectionInput = z.infer<typeof sectionInputSchema>;
|
|
149
|
+
|
|
150
|
+
const citationSchema = z
|
|
151
|
+
.object({
|
|
152
|
+
uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
|
|
153
|
+
anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
|
|
154
|
+
title: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
|
|
155
|
+
lineStart: positiveSafeInt,
|
|
156
|
+
lineEnd: positiveSafeInt,
|
|
157
|
+
sourceFingerprint: z.string().regex(FINGERPRINT_PATTERN),
|
|
158
|
+
})
|
|
159
|
+
.strict()
|
|
160
|
+
.superRefine((citation, ctx) => {
|
|
161
|
+
if (citation.lineEnd < citation.lineStart) {
|
|
162
|
+
ctx.addIssue({
|
|
163
|
+
code: z.ZodIssueCode.custom,
|
|
164
|
+
path: ["lineEnd"],
|
|
165
|
+
message: "citation.lineEnd must be >= citation.lineStart",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const diagnosticsSchema = z
|
|
171
|
+
.object({
|
|
172
|
+
reason: z.string().min(1).max(256).optional(),
|
|
173
|
+
candidates: z
|
|
174
|
+
.array(
|
|
175
|
+
z
|
|
176
|
+
.object({
|
|
177
|
+
anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
|
|
178
|
+
line: positiveSafeInt,
|
|
179
|
+
title: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
|
|
180
|
+
headingPath: z
|
|
181
|
+
.array(
|
|
182
|
+
boundedNonEmpty(SECTION_TARGET_BOUNDS.headingPathItemMaxChars)
|
|
183
|
+
)
|
|
184
|
+
.min(1)
|
|
185
|
+
.max(SECTION_TARGET_BOUNDS.headingPathMaxItems),
|
|
186
|
+
occurrence: positiveSafeInt,
|
|
187
|
+
})
|
|
188
|
+
.strict()
|
|
189
|
+
)
|
|
190
|
+
.max(32)
|
|
191
|
+
.optional(),
|
|
192
|
+
candidateCount: z
|
|
193
|
+
.number()
|
|
194
|
+
.int()
|
|
195
|
+
.min(0)
|
|
196
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
197
|
+
.optional(),
|
|
198
|
+
candidatesTruncated: z.boolean().optional(),
|
|
199
|
+
})
|
|
200
|
+
.strict()
|
|
201
|
+
.superRefine((diagnostics, ctx) => {
|
|
202
|
+
const hasCandidates = diagnostics.candidates !== undefined;
|
|
203
|
+
const hasCount = diagnostics.candidateCount !== undefined;
|
|
204
|
+
const hasTruncated = diagnostics.candidatesTruncated !== undefined;
|
|
205
|
+
if (hasCandidates || hasCount || hasTruncated) {
|
|
206
|
+
if (!(hasCandidates && hasCount && hasTruncated)) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
message:
|
|
210
|
+
"diagnostics candidates, candidateCount, and candidatesTruncated are co-required",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* SDK-validated structured output for gno_section.
|
|
218
|
+
* Single object (not union) so McpServer can publish/validate outputSchema.
|
|
219
|
+
*/
|
|
220
|
+
export const sectionOutputSchema = z
|
|
221
|
+
.object({
|
|
222
|
+
schemaVersion: z.literal(SECTION_MCP_SCHEMA_VERSION),
|
|
223
|
+
action: z.enum(["create", "resolve"]),
|
|
224
|
+
uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
|
|
225
|
+
target: sectionTargetInputSchema,
|
|
226
|
+
status: z
|
|
227
|
+
.enum(["exact", "recovered", "ambiguous", "stale", "missing"])
|
|
228
|
+
.optional(),
|
|
229
|
+
currentFingerprint: z.string().regex(FINGERPRINT_PATTERN).optional(),
|
|
230
|
+
diagnostics: diagnosticsSchema.optional(),
|
|
231
|
+
citation: citationSchema.optional(),
|
|
232
|
+
})
|
|
233
|
+
.strict()
|
|
234
|
+
.superRefine((result, ctx) => {
|
|
235
|
+
if (result.action === "create") {
|
|
236
|
+
if (
|
|
237
|
+
result.status !== undefined ||
|
|
238
|
+
result.currentFingerprint !== undefined ||
|
|
239
|
+
result.diagnostics !== undefined ||
|
|
240
|
+
result.citation !== undefined
|
|
241
|
+
) {
|
|
242
|
+
ctx.addIssue({
|
|
243
|
+
code: z.ZodIssueCode.custom,
|
|
244
|
+
message: "create result forbids resolve-only fields",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
result.status === undefined ||
|
|
251
|
+
result.currentFingerprint === undefined
|
|
252
|
+
) {
|
|
253
|
+
ctx.addIssue({
|
|
254
|
+
code: z.ZodIssueCode.custom,
|
|
255
|
+
message: "resolve requires status and currentFingerprint",
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (result.diagnostics === undefined) {
|
|
260
|
+
ctx.addIssue({
|
|
261
|
+
code: z.ZodIssueCode.custom,
|
|
262
|
+
path: ["diagnostics"],
|
|
263
|
+
message: "resolve requires diagnostics",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const navigable =
|
|
267
|
+
result.status === "exact" || result.status === "recovered";
|
|
268
|
+
if (navigable && result.citation === undefined) {
|
|
269
|
+
ctx.addIssue({
|
|
270
|
+
code: z.ZodIssueCode.custom,
|
|
271
|
+
path: ["citation"],
|
|
272
|
+
message: "exact/recovered require citation",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (!navigable && result.citation !== undefined) {
|
|
276
|
+
ctx.addIssue({
|
|
277
|
+
code: z.ZodIssueCode.custom,
|
|
278
|
+
path: ["citation"],
|
|
279
|
+
message: "ambiguous/stale/missing forbid citation",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export type SectionMcpResult = z.infer<typeof sectionOutputSchema>;
|
|
285
|
+
|
|
286
|
+
export const SECTION_MCP_ANNOTATIONS = {
|
|
287
|
+
readOnlyHint: true,
|
|
288
|
+
destructiveHint: false,
|
|
289
|
+
idempotentHint: true,
|
|
290
|
+
openWorldHint: false,
|
|
291
|
+
} as const;
|
|
292
|
+
|
|
293
|
+
async function lookupDocument(
|
|
294
|
+
ctx: ToolContext,
|
|
295
|
+
ref: string
|
|
296
|
+
): Promise<
|
|
297
|
+
{ ok: true; doc: DocumentRow } | { ok: false; code: string; message: string }
|
|
298
|
+
> {
|
|
299
|
+
const parsed = parseRef(ref);
|
|
300
|
+
if ("error" in parsed) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
code: MCP_ERRORS.INVALID_INPUT.code,
|
|
304
|
+
message: `Invalid ref format: ${parsed.error}`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let doc: DocumentRow | null = null;
|
|
309
|
+
switch (parsed.type) {
|
|
310
|
+
case "docid": {
|
|
311
|
+
const result = await ctx.store.getDocumentByDocid(parsed.value);
|
|
312
|
+
doc = result.ok ? result.value : null;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "uri": {
|
|
316
|
+
const result = await ctx.store.getDocumentByUri(parsed.value);
|
|
317
|
+
doc = result.ok ? result.value : null;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case "collPath": {
|
|
321
|
+
if (!(parsed.collection && parsed.relPath)) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
code: MCP_ERRORS.INVALID_INPUT.code,
|
|
325
|
+
message: "Invalid collection/path format",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const canonical = ctx.collections.find(
|
|
329
|
+
(c) => c.name.toLowerCase() === parsed.collection!.toLowerCase()
|
|
330
|
+
);
|
|
331
|
+
const collectionName = canonical?.name ?? parsed.collection;
|
|
332
|
+
const result = await ctx.store.getDocument(
|
|
333
|
+
collectionName,
|
|
334
|
+
parsed.relPath
|
|
335
|
+
);
|
|
336
|
+
doc = result.ok ? result.value : null;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!doc) {
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
code: MCP_ERRORS.NOT_FOUND.code,
|
|
345
|
+
message: `Document not found: ${ref}`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return { ok: true, doc };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function loadIndexedContent(
|
|
352
|
+
ctx: ToolContext,
|
|
353
|
+
ref: string
|
|
354
|
+
): Promise<{ doc: DocumentRow; content: string }> {
|
|
355
|
+
const lookup = await lookupDocument(ctx, ref);
|
|
356
|
+
if (!lookup.ok) {
|
|
357
|
+
throw new Error(`${lookup.code}: ${lookup.message}`);
|
|
358
|
+
}
|
|
359
|
+
const { doc } = lookup;
|
|
360
|
+
if (!doc.mirrorHash) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`${MCP_ERRORS.NOT_FOUND.code}: Document content unavailable`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const contentResult = await ctx.store.getContent(doc.mirrorHash);
|
|
366
|
+
if (!contentResult.ok || contentResult.value === null) {
|
|
367
|
+
throw new Error("RUNTIME: Mirror content unavailable");
|
|
368
|
+
}
|
|
369
|
+
if (!isTransportBoundedCanonicalUri(doc.uri)) {
|
|
370
|
+
throw new Error(`VALIDATION: ${CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS}`);
|
|
371
|
+
}
|
|
372
|
+
return { doc, content: contentResult.value };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function formatSectionResult(data: SectionMcpResult): string {
|
|
376
|
+
const json = JSON.stringify(data, null, 2);
|
|
377
|
+
if (data.action === "create") {
|
|
378
|
+
return [
|
|
379
|
+
`Created section target for ${data.uri} (anchor=${data.target.anchor}, line=${data.target.hints.line}).`,
|
|
380
|
+
"",
|
|
381
|
+
json,
|
|
382
|
+
].join("\n");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (data.citation) {
|
|
386
|
+
const citation: SectionCitationV1 = data.citation;
|
|
387
|
+
const lineCount = citation.lineEnd - citation.lineStart + 1;
|
|
388
|
+
return [
|
|
389
|
+
`Resolved section (${data.status}) in ${citation.uri}: ${citation.title} (#${citation.anchor}), lines ${citation.lineStart}-${citation.lineEnd}.`,
|
|
390
|
+
`Follow up with gno_get: {"ref":${JSON.stringify(citation.uri)},"fromLine":${citation.lineStart},"lineCount":${lineCount}}`,
|
|
391
|
+
"",
|
|
392
|
+
json,
|
|
393
|
+
].join("\n");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const status = data.status as SectionResolutionStatus;
|
|
397
|
+
return [
|
|
398
|
+
`Section resolution status is ${status}; this result is not safe to navigate or cite.`,
|
|
399
|
+
"",
|
|
400
|
+
json,
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function handleCreate(
|
|
405
|
+
ctx: ToolContext,
|
|
406
|
+
ref: string,
|
|
407
|
+
selectorRaw: { anchor?: string; line?: number }
|
|
408
|
+
): Promise<SectionMcpResult> {
|
|
409
|
+
const selector = parseSectionTargetCreateSelector(selectorRaw);
|
|
410
|
+
if (!selector.ok) {
|
|
411
|
+
throw new Error(`VALIDATION: ${selector.error}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const { doc, content } = await loadIndexedContent(ctx, ref);
|
|
415
|
+
const target = await createSectionTarget({
|
|
416
|
+
content,
|
|
417
|
+
uri: doc.uri,
|
|
418
|
+
...selector.value,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!target) {
|
|
422
|
+
const sections = extractSections(content);
|
|
423
|
+
const matched =
|
|
424
|
+
selector.value.anchor !== undefined
|
|
425
|
+
? sections.some((section) => section.anchor === selector.value.anchor)
|
|
426
|
+
: sections.some((section) => section.line === selector.value.line);
|
|
427
|
+
if (!matched) {
|
|
428
|
+
throw new Error(`${MCP_ERRORS.NOT_FOUND.code}: Section not found`);
|
|
429
|
+
}
|
|
430
|
+
throw new Error("VALIDATION: Section target exceeds size bounds");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const projected = projectSectionTargetCreateResult(doc.uri, target);
|
|
434
|
+
return {
|
|
435
|
+
schemaVersion: SECTION_MCP_SCHEMA_VERSION,
|
|
436
|
+
action: "create",
|
|
437
|
+
...projected,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function handleResolve(
|
|
442
|
+
ctx: ToolContext,
|
|
443
|
+
ref: string,
|
|
444
|
+
targetRaw: unknown
|
|
445
|
+
): Promise<SectionMcpResult> {
|
|
446
|
+
const parsed = parseSectionTargetV1(targetRaw);
|
|
447
|
+
if (!parsed.ok) {
|
|
448
|
+
throw new Error(`VALIDATION: ${parsed.error}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const { doc, content } = await loadIndexedContent(ctx, ref);
|
|
452
|
+
const resolution = await resolveSectionTarget({
|
|
453
|
+
content,
|
|
454
|
+
target: parsed.value,
|
|
455
|
+
uri: doc.uri,
|
|
456
|
+
});
|
|
457
|
+
const projected = projectSectionTargetResolveResult(doc.uri, resolution);
|
|
458
|
+
|
|
459
|
+
if (projected.citation) {
|
|
460
|
+
return {
|
|
461
|
+
schemaVersion: SECTION_MCP_SCHEMA_VERSION,
|
|
462
|
+
action: "resolve",
|
|
463
|
+
uri: projected.uri,
|
|
464
|
+
status: projected.status as "exact" | "recovered",
|
|
465
|
+
currentFingerprint: projected.currentFingerprint,
|
|
466
|
+
target: projected.target,
|
|
467
|
+
diagnostics: projected.diagnostics as SectionResolutionDiagnostics,
|
|
468
|
+
citation: projected.citation,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
schemaVersion: SECTION_MCP_SCHEMA_VERSION,
|
|
474
|
+
action: "resolve",
|
|
475
|
+
uri: projected.uri,
|
|
476
|
+
status: projected.status as "ambiguous" | "stale" | "missing",
|
|
477
|
+
currentFingerprint: projected.currentFingerprint,
|
|
478
|
+
target: projected.target,
|
|
479
|
+
diagnostics: projected.diagnostics as SectionResolutionDiagnostics,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Handle gno_section tool call. Read-only — never mutates store or disk.
|
|
485
|
+
*/
|
|
486
|
+
export function handleSection(
|
|
487
|
+
args: SectionInput,
|
|
488
|
+
ctx: ToolContext
|
|
489
|
+
): Promise<ToolResult> {
|
|
490
|
+
return runTool(
|
|
491
|
+
ctx,
|
|
492
|
+
"gno_section",
|
|
493
|
+
async () => {
|
|
494
|
+
if (args.action === "create") {
|
|
495
|
+
if (args.anchor !== undefined) {
|
|
496
|
+
return handleCreate(ctx, args.ref, { anchor: args.anchor });
|
|
497
|
+
}
|
|
498
|
+
if (args.line !== undefined) {
|
|
499
|
+
return handleCreate(ctx, args.ref, { line: args.line });
|
|
500
|
+
}
|
|
501
|
+
throw new Error(
|
|
502
|
+
"VALIDATION: create requires exactly one of anchor|line"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (args.target === undefined) {
|
|
506
|
+
throw new Error("VALIDATION: resolve requires target");
|
|
507
|
+
}
|
|
508
|
+
return handleResolve(ctx, args.ref, args.target);
|
|
509
|
+
},
|
|
510
|
+
formatSectionResult
|
|
511
|
+
);
|
|
512
|
+
}
|
package/src/sdk/client.ts
CHANGED
|
@@ -47,6 +47,10 @@ import type {
|
|
|
47
47
|
KnowledgeImpactInput,
|
|
48
48
|
KnowledgeImpactResult,
|
|
49
49
|
ListKnowledgeChangesInput,
|
|
50
|
+
SectionTargetCreateResult,
|
|
51
|
+
SectionTargetCreateSelector,
|
|
52
|
+
SectionTargetResolveResult,
|
|
53
|
+
SectionTargetV1,
|
|
50
54
|
} from "./types";
|
|
51
55
|
|
|
52
56
|
import {
|
|
@@ -122,7 +126,17 @@ import {
|
|
|
122
126
|
RETRIEVAL_TRACE_METADATA,
|
|
123
127
|
type RetrievalTraceSession,
|
|
124
128
|
} from "../core/retrieval-trace-session";
|
|
125
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
131
|
+
createSectionTarget as createSectionTargetCore,
|
|
132
|
+
extractSections,
|
|
133
|
+
isTransportBoundedCanonicalUri,
|
|
134
|
+
parseSectionTargetCreateSelector,
|
|
135
|
+
parseSectionTargetV1,
|
|
136
|
+
projectSectionTargetCreateResult,
|
|
137
|
+
projectSectionTargetResolveResult,
|
|
138
|
+
resolveSectionTarget as resolveSectionTargetCore,
|
|
139
|
+
} from "../core/sections";
|
|
126
140
|
import { normalizeStructuredQueryInput } from "../core/structured-query";
|
|
127
141
|
import { parseAndValidateTagFilter } from "../core/tags";
|
|
128
142
|
import {
|
|
@@ -1835,6 +1849,62 @@ class GnoClientImpl implements GnoClient {
|
|
|
1835
1849
|
return extractSections(document.content);
|
|
1836
1850
|
}
|
|
1837
1851
|
|
|
1852
|
+
async createSectionTarget(
|
|
1853
|
+
ref: string,
|
|
1854
|
+
selector: SectionTargetCreateSelector
|
|
1855
|
+
): Promise<SectionTargetCreateResult> {
|
|
1856
|
+
this.assertOpen();
|
|
1857
|
+
const parsed = parseSectionTargetCreateSelector(selector);
|
|
1858
|
+
if (!parsed.ok) {
|
|
1859
|
+
throw sdkError("VALIDATION", parsed.error);
|
|
1860
|
+
}
|
|
1861
|
+
const document = await getDocumentByRef(this.store, this.config, ref, {});
|
|
1862
|
+
// Top-level response uri shares schema maxLength — reject before create.
|
|
1863
|
+
if (!isTransportBoundedCanonicalUri(document.uri)) {
|
|
1864
|
+
throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS);
|
|
1865
|
+
}
|
|
1866
|
+
// Canonical identity from stored document — never from caller.
|
|
1867
|
+
const target = await createSectionTargetCore({
|
|
1868
|
+
content: document.content,
|
|
1869
|
+
uri: document.uri,
|
|
1870
|
+
...parsed.value,
|
|
1871
|
+
});
|
|
1872
|
+
if (!target) {
|
|
1873
|
+
const sections = extractSections(document.content);
|
|
1874
|
+
const matched =
|
|
1875
|
+
parsed.value.anchor !== undefined
|
|
1876
|
+
? sections.some((section) => section.anchor === parsed.value.anchor)
|
|
1877
|
+
: sections.some((section) => section.line === parsed.value.line);
|
|
1878
|
+
if (!matched) {
|
|
1879
|
+
throw sdkError("NOT_FOUND", "Section not found");
|
|
1880
|
+
}
|
|
1881
|
+
throw sdkError("VALIDATION", "Section target exceeds size bounds");
|
|
1882
|
+
}
|
|
1883
|
+
return projectSectionTargetCreateResult(document.uri, target);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async resolveSectionTarget(
|
|
1887
|
+
ref: string,
|
|
1888
|
+
target: SectionTargetV1
|
|
1889
|
+
): Promise<SectionTargetResolveResult> {
|
|
1890
|
+
this.assertOpen();
|
|
1891
|
+
const parsed = parseSectionTargetV1(target);
|
|
1892
|
+
if (!parsed.ok) {
|
|
1893
|
+
throw sdkError("VALIDATION", parsed.error);
|
|
1894
|
+
}
|
|
1895
|
+
const document = await getDocumentByRef(this.store, this.config, ref, {});
|
|
1896
|
+
// Top-level uri must fit schema — citation fail-closed does not repair it.
|
|
1897
|
+
if (!isTransportBoundedCanonicalUri(document.uri)) {
|
|
1898
|
+
throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS);
|
|
1899
|
+
}
|
|
1900
|
+
const resolution = await resolveSectionTargetCore({
|
|
1901
|
+
content: document.content,
|
|
1902
|
+
target: parsed.value,
|
|
1903
|
+
uri: document.uri,
|
|
1904
|
+
});
|
|
1905
|
+
return projectSectionTargetResolveResult(document.uri, resolution);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1838
1908
|
async close(): Promise<void> {
|
|
1839
1909
|
if (this.closed) {
|
|
1840
1910
|
return;
|
package/src/sdk/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type { IndexStatus } from "../store/types";
|
|
|
28
28
|
export { GnoSdkError, sdkError } from "./errors";
|
|
29
29
|
export { createGnoClient } from "./client";
|
|
30
30
|
export type {
|
|
31
|
+
DocumentSection,
|
|
31
32
|
GnoAskOptions,
|
|
32
33
|
GnoCaptureOptions,
|
|
33
34
|
GnoCaptureResult,
|
|
@@ -64,6 +65,10 @@ export type {
|
|
|
64
65
|
KnowledgeImpactInput,
|
|
65
66
|
KnowledgeImpactResult,
|
|
66
67
|
ListKnowledgeChangesInput,
|
|
68
|
+
SectionTargetCreateResult,
|
|
69
|
+
SectionTargetCreateSelector,
|
|
70
|
+
SectionTargetResolveResult,
|
|
71
|
+
SectionTargetV1,
|
|
67
72
|
} from "./types";
|
|
68
73
|
export {
|
|
69
74
|
ContextCapsuleContractError,
|
package/src/sdk/types.ts
CHANGED
|
@@ -52,7 +52,13 @@ import type {
|
|
|
52
52
|
RetrievalTraceListResult,
|
|
53
53
|
RetrievalTracePurgeResult as RetrievalTraceManagementPurgeResult,
|
|
54
54
|
} from "../core/retrieval-trace-management";
|
|
55
|
-
import type {
|
|
55
|
+
import type {
|
|
56
|
+
DocumentSection,
|
|
57
|
+
SectionTargetCreateResult,
|
|
58
|
+
SectionTargetCreateSelector,
|
|
59
|
+
SectionTargetResolveResult,
|
|
60
|
+
SectionTargetV1,
|
|
61
|
+
} from "../core/sections";
|
|
56
62
|
import type { SyncResult } from "../ingestion";
|
|
57
63
|
import type { DownloadPolicy } from "../llm/policy";
|
|
58
64
|
import type {
|
|
@@ -67,10 +73,15 @@ import type { IndexStatus } from "../store/types";
|
|
|
67
73
|
export type {
|
|
68
74
|
AskResult,
|
|
69
75
|
Config,
|
|
76
|
+
DocumentSection,
|
|
70
77
|
DownloadPolicy,
|
|
71
78
|
IndexStatus,
|
|
72
79
|
SearchOptions,
|
|
73
80
|
SearchResults,
|
|
81
|
+
SectionTargetCreateResult,
|
|
82
|
+
SectionTargetCreateSelector,
|
|
83
|
+
SectionTargetResolveResult,
|
|
84
|
+
SectionTargetV1,
|
|
74
85
|
SyncResult,
|
|
75
86
|
};
|
|
76
87
|
export type { AskOptions, HybridSearchOptions } from "../pipeline/types";
|
|
@@ -345,5 +356,22 @@ export interface GnoClient {
|
|
|
345
356
|
options: GnoDuplicateNoteOptions
|
|
346
357
|
): Promise<GnoRefactorNoteResult>;
|
|
347
358
|
getSections(ref: string): Promise<DocumentSection[]>;
|
|
359
|
+
/**
|
|
360
|
+
* Create a durable SectionTargetV1 for a heading in a stored document.
|
|
361
|
+
* Provide exactly one of `anchor` or `line`. Canonical URI comes from the
|
|
362
|
+
* resolved stored document, never from the caller.
|
|
363
|
+
*/
|
|
364
|
+
createSectionTarget(
|
|
365
|
+
ref: string,
|
|
366
|
+
selector: SectionTargetCreateSelector
|
|
367
|
+
): Promise<SectionTargetCreateResult>;
|
|
368
|
+
/**
|
|
369
|
+
* Conservatively resolve a SectionTargetV1 against a stored document.
|
|
370
|
+
* Navigable results include citation evidence; ambiguous/stale/missing do not.
|
|
371
|
+
*/
|
|
372
|
+
resolveSectionTarget(
|
|
373
|
+
ref: string,
|
|
374
|
+
target: SectionTargetV1
|
|
375
|
+
): Promise<SectionTargetResolveResult>;
|
|
348
376
|
close(): Promise<void>;
|
|
349
377
|
}
|