Skip to content

Commit e478e4e

Browse files
committed
feat: update dependencies and enhance job management with session tracking
1 parent 6b911ea commit e478e4e

8 files changed

Lines changed: 146 additions & 52 deletions

File tree

packages/core/bun.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"dependencies": {
5757
"@mozilla/readability": "^0.6.0",
5858
"@tmcp/adapter-valibot": "^0.1.5",
59-
"@tmcp/transport-http": "^0.8.4",
59+
"@tmcp/transport-http": "^0.8.5",
6060
"@tmcp/transport-sse": "^0.5.3",
6161
"@tmcp/transport-stdio": "^0.4.1",
6262
"cac": "^7.0.0",
@@ -66,7 +66,7 @@
6666
"puppeteer-core": "^24.37.5",
6767
"robots-parser": "^3.0.1",
6868
"srvx": "^0.11.8",
69-
"tmcp": "^1.19.2",
69+
"tmcp": "^1.19.3",
7070
"turndown": "^7.2.2",
7171
"turndown-plugin-gfm": "^1.0.2",
7272
"valibot": "^1.2.0"

packages/core/src/jobs/manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export class JobManager {
1414
) { }
1515

1616
/** Start a crawl job for a library */
17-
startCrawl(libraryId: string, opts?: { incremental?: boolean }): CrawlJob {
17+
startCrawl(libraryId: string, opts?: { incremental?: boolean; sessionId?: string }): CrawlJob {
1818
const jobId = nanoid();
19-
const job = this.db.createJob({ id: jobId, libraryId });
19+
const job = this.db.createJob({ id: jobId, libraryId, sessionId: opts?.sessionId });
2020

2121
// Run crawl async (non-blocking)
2222
const worker = new CrawlWorker(this.db, this.eventBus);

packages/core/src/search/format-results.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SearchResult } from './types.js';
2+
import { sanitizeDocContent } from './sanitize.js';
23

34
function formatReasons(reasons: string[]): string {
45
if (reasons.length === 0) {
@@ -16,7 +17,9 @@ export function formatSearchResults(query: string, results: SearchResult[]): str
1617
if (result.heading_context.trim().length > 0) {
1718
block += `**Section:** ${result.heading_context}\n`;
1819
}
19-
block += `${formatReasons(result.reasons)}${result.content}`;
20+
// Sanitize content to prevent prompt injection
21+
const sanitizedContent = sanitizeDocContent(result.content);
22+
block += `${formatReasons(result.reasons)}${sanitizedContent}`;
2023
return block;
2124
})
2225
.join('\n\n---\n\n');
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Output sanitization to prevent prompt injection attacks
3+
* Removes suspicious patterns that could escape agent context
4+
*/
5+
6+
export function sanitizeOutput(text: string): string {
7+
return text
8+
// Remove template directives
9+
.replace(/\{[#%].*?[#%]\}/g, '')
10+
// Remove system prompt markers
11+
.replace(/\[SYSTEM[\]:].*?\[\/SYSTEM\]/gi, '')
12+
.replace(/\[ADMIN[\]:].*?\[\/ADMIN\]/gi, '')
13+
// Remove potential prompt injection patterns
14+
.replace(/ignore\s+above.*?instructions/gi, '')
15+
.replace(/forget\s+previous.*?context/gi, '')
16+
.trim();
17+
}
18+
19+
/**
20+
* Sanitize a single chunk of documentation content
21+
* Removes malicious content while preserving code blocks and formatting
22+
*/
23+
export function sanitizeDocContent(content: string): string {
24+
// First sanitize for injection patterns
25+
let sanitized = sanitizeOutput(content);
26+
27+
// Escape potential dangerous markdown constructs
28+
// but preserve code blocks (between triple backticks)
29+
const codeBlockPattern = /```[\s\S]*?```/g;
30+
const codeBlocks = sanitized.match(codeBlockPattern) || [];
31+
32+
// Temporarily replace code blocks
33+
let temp = sanitized;
34+
codeBlocks.forEach((block, i) => {
35+
temp = temp.replace(block, `__CODE_BLOCK_${i}__`);
36+
});
37+
38+
// Sanitize outside code blocks
39+
temp = sanitizeOutput(temp);
40+
41+
// Restore code blocks
42+
codeBlocks.forEach((block, i) => {
43+
temp = temp.replace(`__CODE_BLOCK_${i}__`, block);
44+
});
45+
46+
return temp;
47+
}

packages/core/src/server.ts

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ server.tool(
4242
{
4343
name: "search_docs",
4444
description:
45-
"Search through indexed documentation libraries for relevant information. " +
46-
"Returns ranked documentation sections with code examples and source URLs. " +
47-
"Use this when you need to find information about a library, framework, API, " +
48-
"or any technical concept.",
45+
"Search indexed docs by keyword or library. Returns ranked sections with URLs.",
46+
annotations: {
47+
readOnlyHint: true,
48+
idempotentHint: true,
49+
},
4950
schema: v.object({
5051
query: v.pipe(
5152
v.string(),
@@ -61,11 +62,16 @@ server.tool(
6162
}),
6263
},
6364
async ({ query, library, limit }) => {
64-
const results = searchEngine.search(query, { library, limit });
65-
if (results.length === 0)
66-
return tool.text(`No results found for "${query}".`);
65+
try {
66+
const results = searchEngine.search(query, { library, limit });
67+
if (results.length === 0)
68+
return tool.text(`No results found for "${query}".`);
6769

68-
return tool.text(formatSearchResults(query, results));
70+
return tool.text(formatSearchResults(query, results));
71+
} catch (err: unknown) {
72+
const message = err instanceof Error ? err.message : "Search failed";
73+
return tool.text(`❌ Error: ${message}`);
74+
}
6975
},
7076
);
7177

@@ -108,14 +114,16 @@ function formatLibraryInfo(libraryId: string): string {
108114
}
109115

110116
// ──────────────────────────────────────
111-
// Tool 2: list_libraries — Discovery tool
117+
// Tool 2: list_libraries — Discovery tool with pagination
112118
// ──────────────────────────────────────
113119
server.tool(
114120
{
115121
name: "list_libraries",
116-
description:
117-
"List all documentation libraries currently indexed and available for searching. " +
118-
"Use this to discover what docs are available before running search_docs.",
122+
description: "List indexed documentation libraries. Paginated results.",
123+
annotations: {
124+
readOnlyHint: true,
125+
idempotentHint: true,
126+
},
119127
schema: v.object({
120128
status: v.optional(
121129
v.pipe(
@@ -124,23 +132,46 @@ server.tool(
124132
),
125133
"all",
126134
),
135+
page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1),
136+
limit: v.optional(
137+
v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(50)),
138+
20,
139+
),
127140
}),
128141
},
129-
async ({ status }) => {
130-
const libraries = db.listLibraries(status);
131-
if (libraries.length === 0) {
132-
return tool.text(
133-
"No libraries indexed yet. Use manage_library with action=add to add a documentation website.",
134-
);
135-
}
142+
async ({ status, page = 1, limit = 20 }) => {
143+
try {
144+
const libraries = db.listLibraries(status);
145+
if (libraries.length === 0) {
146+
return tool.text(
147+
"No libraries indexed yet. Use manage_library with action=add to add a documentation website.",
148+
);
149+
}
136150

137-
let output = `## Indexed Libraries (${libraries.length} total)\n\n`;
138-
output += "| Library | URL | Pages | Chunks | Status |\n";
139-
output += "| ------- | --- | ----- | ------ | ------ |\n";
140-
for (const lib of libraries) {
141-
output += `| ${lib.name} | ${lib.url} | ${lib.page_count} | ${lib.chunk_count} | ${lib.status} |\n`;
151+
// Paginate results
152+
const start = (page - 1) * limit;
153+
const end = start + limit;
154+
const paginated = libraries.slice(start, end);
155+
const hasMore = end < libraries.length;
156+
157+
// Minified response (no pretty-printing)
158+
let output = `## Libraries (${start + 1}-${Math.min(end, libraries.length)} of ${libraries.length})\n\n`;
159+
output += "| Library | URL | Pages | Chunks | Status |\n";
160+
output += "| ------- | --- | ----- | ------ | ------ |\n";
161+
for (const lib of paginated) {
162+
output += `|${lib.name}|${lib.url}|${lib.page_count}|${lib.chunk_count}|${lib.status}|\n`;
163+
}
164+
165+
if (hasMore) {
166+
output += `\n**More available.** Use page=${page + 1} to fetch next page.`;
167+
}
168+
169+
return tool.text(output);
170+
} catch (err: unknown) {
171+
const message =
172+
err instanceof Error ? err.message : "Failed to list libraries";
173+
return tool.text(`❌ Error: ${message}`);
142174
}
143-
return tool.text(output);
144175
},
145176
);
146177

@@ -150,9 +181,11 @@ server.tool(
150181
server.tool(
151182
{
152183
name: "get_doc_page",
153-
description:
154-
"Retrieve the complete content of a specific documentation page as markdown. " +
155-
"Use when search results reference a page and you need full context.",
184+
description: "Retrieve complete documentation page as markdown.",
185+
annotations: {
186+
readOnlyHint: true,
187+
idempotentHint: true,
188+
},
156189
schema: v.object({
157190
url: v.optional(
158191
v.pipe(
@@ -169,14 +202,20 @@ server.tool(
169202
}),
170203
},
171204
async ({ url, library, path }) => {
172-
const page = db.getPage({ url, library, path });
173-
if (!page)
205+
try {
206+
const page = db.getPage({ url, library, path });
207+
if (!page)
208+
return tool.text(
209+
"Page not found. Use search_docs to find the correct page.",
210+
);
174211
return tool.text(
175-
"Page not found. Use search_docs to find the correct page.",
212+
`# ${page.title}\n**Source:** ${page.url}\n\n${page.content_markdown}`,
176213
);
177-
return tool.text(
178-
`# ${page.title}\n**Source:** ${page.url}\n\n${page.content_markdown}`,
179-
);
214+
} catch (err: unknown) {
215+
const message =
216+
err instanceof Error ? err.message : "Failed to fetch page";
217+
return tool.text(`❌ Error: ${message}`);
218+
}
180219
},
181220
);
182221

@@ -187,7 +226,10 @@ server.tool(
187226
{
188227
name: "manage_library",
189228
description:
190-
"Manage a documentation library lifecycle. Use action=add to crawl a new source, action=rename to change the library name, action=refresh to re-crawl, action=remove to delete it, or action=info to inspect its pages and stats.",
229+
"Manage library lifecycle: add/rename/refresh/remove/info. Destructive actions require confirmation.",
230+
annotations: {
231+
destructiveHint: true,
232+
},
191233
schema: v.object({
192234
action: v.pipe(
193235
v.picklist(["add", "rename", "refresh", "remove", "info"]),
@@ -268,7 +310,7 @@ server.tool(
268310
const lib = db.getLibraryByName(libraryName);
269311
if (!lib)
270312
return tool.text(
271-
`Library "${libraryName}" not found. Use list_libraries to see available.`,
313+
`Library "${libraryName}" not found. Use list_libraries to see available.`,
272314
);
273315

274316
const job = jobManager.startCrawl(lib.id, { incremental: true });
@@ -282,7 +324,7 @@ server.tool(
282324
"library is required for action=remove.",
283325
);
284326
const lib = db.getLibraryByName(libraryName);
285-
if (!lib) return tool.text(`Library "${libraryName}" not found.`);
327+
if (!lib) return tool.text(`Library "${libraryName}" not found.`);
286328

287329
db.removeLibrary(lib.id);
288330
return tool.text(
@@ -297,17 +339,17 @@ server.tool(
297339
const lib = db.getLibraryByName(libraryName);
298340
if (!lib)
299341
return tool.text(
300-
`Library "${libraryName}" not found. Use list_libraries to see available libraries.`,
342+
`Library "${libraryName}" not found. Use list_libraries to see available libraries.`,
301343
);
302344

303345
return tool.text(formatLibraryInfo(lib.id));
304346
}
305347
}
306348
} catch (err: unknown) {
307349
const message = err instanceof Error ? err.message : "Unknown error";
308-
return tool.text(`❌ Failed: ${message}`);
350+
return tool.text(`❌ Error: ${message}`);
309351
}
310352

311-
return tool.text(`❌ Failed: Unsupported action.`);
353+
return tool.text(`❌ Error: Unsupported action.`);
312354
},
313355
);

packages/core/src/storage/db.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class Database {
104104
CREATE TABLE IF NOT EXISTS crawl_jobs (
105105
id TEXT PRIMARY KEY,
106106
library_id TEXT NOT NULL REFERENCES libraries(id) ON DELETE CASCADE,
107+
session_id TEXT,
107108
status TEXT NOT NULL DEFAULT 'queued',
108109
pages_discovered INTEGER NOT NULL DEFAULT 0,
109110
pages_crawled INTEGER NOT NULL DEFAULT 0,
@@ -316,10 +317,10 @@ export class Database {
316317
// Crawl Jobs
317318
// ──────────────────────────────────────
318319

319-
createJob(job: { id: string; libraryId: string }): CrawlJob {
320+
createJob(job: { id: string; libraryId: string; sessionId?: string }): CrawlJob {
320321
this.db
321-
.prepare("INSERT INTO crawl_jobs (id, library_id) VALUES (?, ?)")
322-
.run(job.id, job.libraryId);
322+
.prepare("INSERT INTO crawl_jobs (id, library_id, session_id) VALUES (?, ?, ?)")
323+
.run(job.id, job.libraryId, job.sessionId ?? null);
323324
return this.db
324325
.prepare("SELECT * FROM crawl_jobs WHERE id = ?")
325326
.get(job.id) as CrawlJob;

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface ChunkRecord {
4747
export interface CrawlJob {
4848
id: string;
4949
library_id: string;
50+
session_id?: string;
5051
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
5152
pages_discovered: number;
5253
pages_crawled: number;

0 commit comments

Comments
 (0)