@gmickel/gno 0.3.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 +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_get tool - Retrieve single document.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/get
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join as pathJoin } from 'node:path';
|
|
8
|
+
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { parseRef } from '../../cli/commands/ref-parser';
|
|
10
|
+
import type { DocumentRow, StorePort } from '../../store/types';
|
|
11
|
+
import type { ToolContext } from '../server';
|
|
12
|
+
import { runTool, type ToolResult } from './index';
|
|
13
|
+
|
|
14
|
+
interface GetInput {
|
|
15
|
+
ref: string;
|
|
16
|
+
fromLine?: number;
|
|
17
|
+
lineCount?: number;
|
|
18
|
+
lineNumbers?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GetResponse {
|
|
22
|
+
docid: string;
|
|
23
|
+
uri: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
content: string;
|
|
26
|
+
totalLines: number;
|
|
27
|
+
returnedLines?: { start: number; end: number };
|
|
28
|
+
language?: string;
|
|
29
|
+
source: {
|
|
30
|
+
absPath?: string;
|
|
31
|
+
relPath: string;
|
|
32
|
+
mime: string;
|
|
33
|
+
ext: string;
|
|
34
|
+
modifiedAt?: string;
|
|
35
|
+
sizeBytes?: number;
|
|
36
|
+
sourceHash?: string;
|
|
37
|
+
};
|
|
38
|
+
conversion?: {
|
|
39
|
+
converterId?: string;
|
|
40
|
+
converterVersion?: string;
|
|
41
|
+
mirrorHash?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lookup document by parsed reference.
|
|
47
|
+
*/
|
|
48
|
+
async function lookupDocument(
|
|
49
|
+
store: StorePort,
|
|
50
|
+
parsed: ReturnType<typeof parseRef>
|
|
51
|
+
): Promise<DocumentRow | null> {
|
|
52
|
+
if ('error' in parsed) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
switch (parsed.type) {
|
|
57
|
+
case 'docid': {
|
|
58
|
+
const result = await store.getDocumentByDocid(parsed.value);
|
|
59
|
+
return result.ok ? result.value : null;
|
|
60
|
+
}
|
|
61
|
+
case 'uri': {
|
|
62
|
+
const result = await store.getDocumentByUri(parsed.value);
|
|
63
|
+
return result.ok ? result.value : null;
|
|
64
|
+
}
|
|
65
|
+
case 'collPath': {
|
|
66
|
+
if (!(parsed.collection && parsed.relPath)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const result = await store.getDocument(parsed.collection, parsed.relPath);
|
|
70
|
+
return result.ok ? result.value : null;
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format get response as text.
|
|
79
|
+
*/
|
|
80
|
+
function formatGetResponse(data: GetResponse): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
|
|
83
|
+
lines.push(`Document: ${data.uri}`);
|
|
84
|
+
if (data.title) {
|
|
85
|
+
lines.push(`Title: ${data.title}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push(`Lines: ${data.totalLines}`);
|
|
88
|
+
if (data.source.absPath) {
|
|
89
|
+
lines.push(`Path: ${data.source.absPath}`);
|
|
90
|
+
}
|
|
91
|
+
lines.push('');
|
|
92
|
+
|
|
93
|
+
if (data.returnedLines) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`--- Content (lines ${data.returnedLines.start}-${data.returnedLines.end}) ---`
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
lines.push('--- Content ---');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(data.content);
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle gno_get tool call.
|
|
108
|
+
*/
|
|
109
|
+
export function handleGet(
|
|
110
|
+
args: GetInput,
|
|
111
|
+
ctx: ToolContext
|
|
112
|
+
): Promise<ToolResult> {
|
|
113
|
+
return runTool(
|
|
114
|
+
ctx,
|
|
115
|
+
'gno_get',
|
|
116
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: document retrieval with multiple ref formats and chunk handling
|
|
117
|
+
async () => {
|
|
118
|
+
// Parse reference
|
|
119
|
+
const parsed = parseRef(args.ref);
|
|
120
|
+
if ('error' in parsed) {
|
|
121
|
+
throw new Error(parsed.error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Lookup document
|
|
125
|
+
const doc = await lookupDocument(ctx.store, parsed);
|
|
126
|
+
if (!doc) {
|
|
127
|
+
throw new Error(`Document not found: ${args.ref}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get content
|
|
131
|
+
if (!doc.mirrorHash) {
|
|
132
|
+
throw new Error('Document has no indexed content');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const contentResult = await ctx.store.getContent(doc.mirrorHash);
|
|
136
|
+
if (!contentResult.ok) {
|
|
137
|
+
throw new Error(contentResult.error.message);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fullContent = contentResult.value ?? '';
|
|
141
|
+
const contentLines = fullContent.split('\n');
|
|
142
|
+
const totalLines = contentLines.length;
|
|
143
|
+
|
|
144
|
+
// Apply line range if specified
|
|
145
|
+
let content = fullContent;
|
|
146
|
+
let returnedLines: { start: number; end: number } | undefined;
|
|
147
|
+
|
|
148
|
+
// lineNumbers defaults to true per spec
|
|
149
|
+
const showLineNumbers = args.lineNumbers !== false;
|
|
150
|
+
|
|
151
|
+
if (args.fromLine || args.lineCount) {
|
|
152
|
+
const startLine = args.fromLine ?? 1;
|
|
153
|
+
// Clamp startLine to valid range
|
|
154
|
+
if (startLine > totalLines) {
|
|
155
|
+
// Return empty content for out-of-range request
|
|
156
|
+
content = '';
|
|
157
|
+
returnedLines = undefined;
|
|
158
|
+
} else {
|
|
159
|
+
const count = args.lineCount ?? totalLines - startLine + 1;
|
|
160
|
+
const endLine = Math.min(startLine + count - 1, totalLines);
|
|
161
|
+
|
|
162
|
+
const slicedLines = contentLines.slice(startLine - 1, endLine);
|
|
163
|
+
|
|
164
|
+
if (showLineNumbers) {
|
|
165
|
+
content = slicedLines
|
|
166
|
+
.map((line, i) => `${startLine + i}: ${line}`)
|
|
167
|
+
.join('\n');
|
|
168
|
+
} else {
|
|
169
|
+
content = slicedLines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
returnedLines = { start: startLine, end: endLine };
|
|
173
|
+
}
|
|
174
|
+
} else if (showLineNumbers) {
|
|
175
|
+
content = contentLines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Build absPath
|
|
179
|
+
const uriParsed = parseUri(doc.uri);
|
|
180
|
+
let absPath: string | undefined;
|
|
181
|
+
if (uriParsed) {
|
|
182
|
+
const collection = ctx.collections.find(
|
|
183
|
+
(c) => c.name === uriParsed.collection
|
|
184
|
+
);
|
|
185
|
+
if (collection) {
|
|
186
|
+
absPath = pathJoin(collection.path, doc.relPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const response: GetResponse = {
|
|
191
|
+
docid: doc.docid,
|
|
192
|
+
uri: doc.uri,
|
|
193
|
+
title: doc.title ?? undefined,
|
|
194
|
+
content,
|
|
195
|
+
totalLines,
|
|
196
|
+
returnedLines,
|
|
197
|
+
language: doc.languageHint ?? undefined,
|
|
198
|
+
source: {
|
|
199
|
+
absPath,
|
|
200
|
+
relPath: doc.relPath,
|
|
201
|
+
mime: doc.sourceMime,
|
|
202
|
+
ext: doc.sourceExt,
|
|
203
|
+
modifiedAt: doc.sourceMtime,
|
|
204
|
+
sizeBytes: doc.sourceSize,
|
|
205
|
+
sourceHash: doc.sourceHash,
|
|
206
|
+
},
|
|
207
|
+
conversion: doc.mirrorHash
|
|
208
|
+
? {
|
|
209
|
+
converterId: doc.converterId ?? undefined,
|
|
210
|
+
converterVersion: doc.converterVersion ?? undefined,
|
|
211
|
+
mirrorHash: doc.mirrorHash,
|
|
212
|
+
}
|
|
213
|
+
: undefined,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return response;
|
|
217
|
+
},
|
|
218
|
+
formatGetResponse
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool registration and shared utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { ToolContext } from '../server';
|
|
10
|
+
import { handleGet } from './get';
|
|
11
|
+
import { handleMultiGet } from './multi-get';
|
|
12
|
+
import { handleQuery } from './query';
|
|
13
|
+
import { handleSearch } from './search';
|
|
14
|
+
import { handleStatus } from './status';
|
|
15
|
+
import { handleVsearch } from './vsearch';
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Shared Input Schemas
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const searchInputSchema = z.object({
|
|
22
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
23
|
+
collection: z.string().optional(),
|
|
24
|
+
limit: z.number().int().min(1).max(100).default(5),
|
|
25
|
+
minScore: z.number().min(0).max(1).optional(),
|
|
26
|
+
lang: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const vsearchInputSchema = z.object({
|
|
30
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
31
|
+
collection: z.string().optional(),
|
|
32
|
+
limit: z.number().int().min(1).max(100).default(5),
|
|
33
|
+
minScore: z.number().min(0).max(1).optional(),
|
|
34
|
+
lang: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const queryInputSchema = z.object({
|
|
38
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
39
|
+
collection: z.string().optional(),
|
|
40
|
+
limit: z.number().int().min(1).max(100).default(5),
|
|
41
|
+
minScore: z.number().min(0).max(1).optional(),
|
|
42
|
+
lang: z.string().optional(),
|
|
43
|
+
expand: z.boolean().default(true),
|
|
44
|
+
rerank: z.boolean().default(true),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const getInputSchema = z.object({
|
|
48
|
+
ref: z.string().min(1, 'Reference cannot be empty'),
|
|
49
|
+
fromLine: z.number().int().min(1).optional(),
|
|
50
|
+
lineCount: z.number().int().min(1).optional(),
|
|
51
|
+
lineNumbers: z.boolean().default(true),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const multiGetInputSchema = z.object({
|
|
55
|
+
refs: z.array(z.string()).min(1).optional(),
|
|
56
|
+
pattern: z.string().optional(),
|
|
57
|
+
maxBytes: z.number().int().min(1).default(10_240),
|
|
58
|
+
lineNumbers: z.boolean().default(true),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const statusInputSchema = z.object({});
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Tool Result Type
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface ToolResult {
|
|
68
|
+
[x: string]: unknown;
|
|
69
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
70
|
+
structuredContent?: { [x: string]: unknown };
|
|
71
|
+
isError?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// DRY Helper: Exception Firewall + Mutex + Response Shaping
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export async function runTool<T>(
|
|
79
|
+
ctx: ToolContext,
|
|
80
|
+
name: string,
|
|
81
|
+
fn: () => Promise<T>,
|
|
82
|
+
formatText: (data: T) => string
|
|
83
|
+
): Promise<ToolResult> {
|
|
84
|
+
// Check shutdown
|
|
85
|
+
if (ctx.isShuttingDown()) {
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: 'text', text: 'Error: Server is shutting down' }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sequential execution via mutex
|
|
93
|
+
const release = await ctx.toolMutex.acquire();
|
|
94
|
+
try {
|
|
95
|
+
const data = await fn();
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: formatText(data) }],
|
|
98
|
+
structuredContent: data as { [x: string]: unknown },
|
|
99
|
+
};
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Exception firewall: never throw, always return isError
|
|
102
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
103
|
+
console.error(`[MCP] ${name} error:`, message);
|
|
104
|
+
return {
|
|
105
|
+
isError: true,
|
|
106
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
107
|
+
};
|
|
108
|
+
} finally {
|
|
109
|
+
release();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// Tool Registration
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
118
|
+
// Tool IDs use underscores (MCP pattern: ^[a-zA-Z0-9_-]{1,64}$)
|
|
119
|
+
server.tool(
|
|
120
|
+
'gno_search',
|
|
121
|
+
'BM25 full-text search across indexed documents',
|
|
122
|
+
searchInputSchema.shape,
|
|
123
|
+
(args) => handleSearch(args, ctx)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'gno_vsearch',
|
|
128
|
+
'Vector/semantic similarity search',
|
|
129
|
+
vsearchInputSchema.shape,
|
|
130
|
+
(args) => handleVsearch(args, ctx)
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
134
|
+
'gno_query',
|
|
135
|
+
'Hybrid search with optional expansion and reranking',
|
|
136
|
+
queryInputSchema.shape,
|
|
137
|
+
(args) => handleQuery(args, ctx)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
'gno_get',
|
|
142
|
+
'Retrieve a single document by URI, docid, or collection/path',
|
|
143
|
+
getInputSchema.shape,
|
|
144
|
+
(args) => handleGet(args, ctx)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
'gno_multi_get',
|
|
149
|
+
'Retrieve multiple documents by refs or glob pattern',
|
|
150
|
+
multiGetInputSchema.shape,
|
|
151
|
+
(args) => handleMultiGet(args, ctx)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
'gno_status',
|
|
156
|
+
'Get index status and health information',
|
|
157
|
+
statusInputSchema.shape,
|
|
158
|
+
(args) => handleStatus(args, ctx)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_multi_get tool - Retrieve multiple documents.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/multi-get
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join as pathJoin } from 'node:path';
|
|
8
|
+
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { parseRef } from '../../cli/commands/ref-parser';
|
|
10
|
+
import type { DocumentRow, StorePort } from '../../store/types';
|
|
11
|
+
import type { ToolContext } from '../server';
|
|
12
|
+
import { runTool, type ToolResult } from './index';
|
|
13
|
+
|
|
14
|
+
interface MultiGetInput {
|
|
15
|
+
refs?: string[];
|
|
16
|
+
pattern?: string;
|
|
17
|
+
maxBytes?: number;
|
|
18
|
+
lineNumbers?: boolean; // defaults to true per spec
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DocumentResult {
|
|
22
|
+
docid: string;
|
|
23
|
+
uri: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
content: string;
|
|
26
|
+
totalLines: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
source: {
|
|
29
|
+
absPath?: string;
|
|
30
|
+
relPath: string;
|
|
31
|
+
mime: string;
|
|
32
|
+
ext: string;
|
|
33
|
+
modifiedAt?: string;
|
|
34
|
+
sizeBytes?: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MultiGetResponse {
|
|
39
|
+
documents: DocumentResult[];
|
|
40
|
+
skipped: Array<{ ref: string; reason: string }>;
|
|
41
|
+
meta: {
|
|
42
|
+
requested: number;
|
|
43
|
+
returned: number;
|
|
44
|
+
skipped: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Default per spec is 10240
|
|
49
|
+
const DEFAULT_MAX_BYTES = 10_240;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lookup document by parsed reference.
|
|
53
|
+
*/
|
|
54
|
+
async function lookupDocument(
|
|
55
|
+
store: StorePort,
|
|
56
|
+
parsed: ReturnType<typeof parseRef>
|
|
57
|
+
): Promise<DocumentRow | null> {
|
|
58
|
+
if ('error' in parsed) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
switch (parsed.type) {
|
|
63
|
+
case 'docid': {
|
|
64
|
+
const result = await store.getDocumentByDocid(parsed.value);
|
|
65
|
+
return result.ok ? result.value : null;
|
|
66
|
+
}
|
|
67
|
+
case 'uri': {
|
|
68
|
+
const result = await store.getDocumentByUri(parsed.value);
|
|
69
|
+
return result.ok ? result.value : null;
|
|
70
|
+
}
|
|
71
|
+
case 'collPath': {
|
|
72
|
+
if (!(parsed.collection && parsed.relPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const result = await store.getDocument(parsed.collection, parsed.relPath);
|
|
76
|
+
return result.ok ? result.value : null;
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format multi-get response as text.
|
|
85
|
+
*/
|
|
86
|
+
function formatMultiGetResponse(data: MultiGetResponse): string {
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
|
|
89
|
+
lines.push(
|
|
90
|
+
`Retrieved ${data.meta.returned}/${data.meta.requested} documents`
|
|
91
|
+
);
|
|
92
|
+
if (data.meta.skipped > 0) {
|
|
93
|
+
lines.push(`Skipped: ${data.meta.skipped}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
|
|
97
|
+
for (const doc of data.documents) {
|
|
98
|
+
lines.push(`=== ${doc.uri} ===`);
|
|
99
|
+
if (doc.title) {
|
|
100
|
+
lines.push(`Title: ${doc.title}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(
|
|
103
|
+
`Lines: ${doc.totalLines}${doc.truncated ? ' (truncated)' : ''}`
|
|
104
|
+
);
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(doc.content);
|
|
107
|
+
lines.push('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (data.skipped.length > 0) {
|
|
111
|
+
lines.push('--- Skipped ---');
|
|
112
|
+
for (const s of data.skipped) {
|
|
113
|
+
lines.push(`${s.ref}: ${s.reason}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handle gno_multi_get tool call.
|
|
122
|
+
*/
|
|
123
|
+
export function handleMultiGet(
|
|
124
|
+
args: MultiGetInput,
|
|
125
|
+
ctx: ToolContext
|
|
126
|
+
): Promise<ToolResult> {
|
|
127
|
+
return runTool(
|
|
128
|
+
ctx,
|
|
129
|
+
'gno_multi_get',
|
|
130
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multi-get with pattern expansion and batch retrieval
|
|
131
|
+
async () => {
|
|
132
|
+
// Validate input
|
|
133
|
+
if (!(args.refs || args.pattern)) {
|
|
134
|
+
throw new Error('Either refs or pattern must be provided');
|
|
135
|
+
}
|
|
136
|
+
if (args.refs && args.pattern) {
|
|
137
|
+
throw new Error('Cannot specify both refs and pattern');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const maxBytes = args.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
141
|
+
const documents: DocumentResult[] = [];
|
|
142
|
+
const skipped: Array<{ ref: string; reason: string }> = [];
|
|
143
|
+
|
|
144
|
+
let refs: string[] = args.refs ?? [];
|
|
145
|
+
|
|
146
|
+
// Pattern-based lookup
|
|
147
|
+
if (args.pattern) {
|
|
148
|
+
// For pattern matching, list all documents and filter
|
|
149
|
+
const listResult = await ctx.store.listDocuments();
|
|
150
|
+
if (!listResult.ok) {
|
|
151
|
+
throw new Error(listResult.error.message);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Safe glob-like pattern matching: escape regex metacharacters first
|
|
155
|
+
const pattern = args.pattern;
|
|
156
|
+
// Escape all regex metacharacters except * and ?
|
|
157
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
// Then convert glob wildcards to regex
|
|
159
|
+
const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
160
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
161
|
+
|
|
162
|
+
refs = listResult.value
|
|
163
|
+
.filter((d) => regex.test(d.uri) || regex.test(d.relPath))
|
|
164
|
+
.map((d) => d.uri);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Process each reference
|
|
168
|
+
for (const ref of refs) {
|
|
169
|
+
const parsed = parseRef(ref);
|
|
170
|
+
if ('error' in parsed) {
|
|
171
|
+
skipped.push({ ref, reason: parsed.error });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const doc = await lookupDocument(ctx.store, parsed);
|
|
176
|
+
if (!doc) {
|
|
177
|
+
skipped.push({ ref, reason: 'Not found' });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!doc.mirrorHash) {
|
|
182
|
+
skipped.push({ ref, reason: 'No indexed content' });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get content
|
|
187
|
+
const contentResult = await ctx.store.getContent(doc.mirrorHash);
|
|
188
|
+
if (!contentResult.ok) {
|
|
189
|
+
skipped.push({ ref, reason: contentResult.error.message });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let content = contentResult.value ?? '';
|
|
194
|
+
let truncated = false;
|
|
195
|
+
|
|
196
|
+
// Apply maxBytes truncation (actual UTF-8 bytes, not characters)
|
|
197
|
+
const contentBuffer = Buffer.from(content, 'utf8');
|
|
198
|
+
if (contentBuffer.length > maxBytes) {
|
|
199
|
+
// Truncate by bytes, then decode safely (may cut mid-codepoint)
|
|
200
|
+
const truncatedBuffer = contentBuffer.subarray(0, maxBytes);
|
|
201
|
+
// Decode with replacement char for incomplete sequences
|
|
202
|
+
content = truncatedBuffer.toString('utf8');
|
|
203
|
+
// Remove potential trailing replacement char from cut codepoint
|
|
204
|
+
if (content.endsWith('\uFFFD')) {
|
|
205
|
+
content = content.slice(0, -1);
|
|
206
|
+
}
|
|
207
|
+
truncated = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const contentLines = content.split('\n');
|
|
211
|
+
|
|
212
|
+
// Apply line numbers (defaults to true per spec)
|
|
213
|
+
if (args.lineNumbers !== false) {
|
|
214
|
+
content = contentLines
|
|
215
|
+
.map((line, i) => `${i + 1}: ${line}`)
|
|
216
|
+
.join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build absPath
|
|
220
|
+
const uriParsed = parseUri(doc.uri);
|
|
221
|
+
let absPath: string | undefined;
|
|
222
|
+
if (uriParsed) {
|
|
223
|
+
const collection = ctx.collections.find(
|
|
224
|
+
(c) => c.name === uriParsed.collection
|
|
225
|
+
);
|
|
226
|
+
if (collection) {
|
|
227
|
+
absPath = pathJoin(collection.path, doc.relPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
documents.push({
|
|
232
|
+
docid: doc.docid,
|
|
233
|
+
uri: doc.uri,
|
|
234
|
+
title: doc.title ?? undefined,
|
|
235
|
+
content,
|
|
236
|
+
totalLines: (contentResult.value ?? '').split('\n').length,
|
|
237
|
+
truncated,
|
|
238
|
+
source: {
|
|
239
|
+
absPath,
|
|
240
|
+
relPath: doc.relPath,
|
|
241
|
+
mime: doc.sourceMime,
|
|
242
|
+
ext: doc.sourceExt,
|
|
243
|
+
modifiedAt: doc.sourceMtime,
|
|
244
|
+
sizeBytes: doc.sourceSize,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const response: MultiGetResponse = {
|
|
250
|
+
documents,
|
|
251
|
+
skipped,
|
|
252
|
+
meta: {
|
|
253
|
+
requested: refs.length,
|
|
254
|
+
returned: documents.length,
|
|
255
|
+
skipped: skipped.length,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return response;
|
|
260
|
+
},
|
|
261
|
+
formatMultiGetResponse
|
|
262
|
+
);
|
|
263
|
+
}
|