From 0334c5836b3d2f521d3727181ae4033f37593ec8 Mon Sep 17 00:00:00 2001 From: bendtherules Date: Mon, 20 Apr 2026 18:32:59 +0530 Subject: [PATCH] getSectionContent - allow multiple sectionids --- src/agent-tools/getSectionContent.ts | 82 ++++++++++++++++---- src/mcp-server-http.ts | 16 ++-- src/mcp-server-stdio.ts | 8 +- src/test/manual/test-mcp-server.ts | 13 ++-- src/test/manual/test-nonexistent-sections.ts | 70 +++++++++++++++++ 5 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 src/test/manual/test-nonexistent-sections.ts diff --git a/src/agent-tools/getSectionContent.ts b/src/agent-tools/getSectionContent.ts index 1ea9196..7b7f6e5 100644 --- a/src/agent-tools/getSectionContent.ts +++ b/src/agent-tools/getSectionContent.ts @@ -14,9 +14,18 @@ const sectionContentSchema = z.object({ partIndex: z.number().optional(), }); -const getSectionContentOutputSchema = z.object({ +const sectionDataSchema = z.object({ + sectionId: z.string(), content: z.string(), - sectionCount: z.number(), + found: z.boolean(), + error: z.string().optional(), + sectionTitle: z.string().optional(), + partIndex: z.number().optional(), + totalParts: z.number().optional(), +}); + +const getSectionContentOutputSchema = z.object({ + sections: z.array(sectionDataSchema), }); // #endregion @@ -25,20 +34,21 @@ const getSectionContentOutputSchema = z.object({ export const toolMetadata = { description: - "Retrieves all text chunks from a specific specification section by sectionid. " + - "Supports recursive fetching - if recursive=true and the section has children, it will fetch all descendants. " + + "Retrieves all text chunks from one or more specification sections by section IDs. " + + "Supports recursive fetching - if recursive=true and a section has children, it will fetch all descendants. " + "Use this to get complete content when you see 'Subsection available' or 'partial section' references.", args: { - sectionId: "The section ID (e.g., 'sec-if-statement') to fetch chunks for", + sectionIds: + "Array of section IDs (e.g., ['sec-if-statement', 'sec-for-statement']) to fetch chunks for", recursive: "If true, recursively fetches content from all child sections and their descendants. " + - "If false, only returns content from the specified section itself. " + - "Use false when you only need the specific section's content without subsections.", + "If false, only returns content from the specified sections themselves. " + + "Use false when you only need the specific sections' content without subsections.", }, }; export const inputSchema = z.object({ - sectionId: z.string().describe(toolMetadata.args.sectionId), + sectionIds: z.array(z.string()).describe(toolMetadata.args.sectionIds), recursive: z.boolean().default(true).describe(toolMetadata.args.recursive), }); @@ -69,11 +79,19 @@ export type GetSectionContentInput = z.infer; */ export function createGetSectionContentTool(table: Table) { return async ({ - sectionId, + sectionIds, recursive, }: GetSectionContentInput): Promise => { - const allDocs: string[] = []; - const queue: string[] = [sectionId]; + const sectionsData = new Map< + string, + { + content: string[]; + title?: string; + partIndex?: number; + totalParts?: number; + } + >(); + const queue: string[] = [...sectionIds]; const visited = new Set(); while (queue.length > 0) { @@ -84,7 +102,7 @@ export function createGetSectionContentTool(table: Table) { const results = await table .query() .where(`sectionid = '${currentId}'`) - .limit(100) + .limit(10) .toArray(); // Sort by partindex to maintain order (nulls last for single-part sections) @@ -99,10 +117,23 @@ export function createGetSectionContentTool(table: Table) { text?: string; childrensectionids?: string[]; sectiontitle?: string; + partindex?: number; + totalparts?: number; }; if (typedResult.text) { - allDocs.push(typedResult.text); + // Get or create section data + let section = sectionsData.get(currentId); + if (!section) { + section = { + content: [], + title: typedResult.sectiontitle, + partIndex: typedResult.partindex ?? undefined, + totalParts: typedResult.totalparts ?? undefined, + }; + sectionsData.set(currentId, section); + } + section.content.push(typedResult.text); } // Add children to queue for recursive fetching only if recursive is true @@ -116,9 +147,30 @@ export function createGetSectionContentTool(table: Table) { } } + // Build output array from all requested sections + // Missing sections are included with found: false and error message + const sections = sectionIds.map((id) => { + const data = sectionsData.get(id); + if (data) { + return { + sectionId: id, + content: data.content.join("\n\n"), + found: true, + sectionTitle: data.title, + partIndex: data.partIndex, + totalParts: data.totalParts, + }; + } + return { + sectionId: id, + content: "", + found: false, + error: `Section '${id}' not found in database`, + }; + }); + return { - content: allDocs.join("\n\n---\n\n"), - sectionCount: visited.size, + sections, }; }; } diff --git a/src/mcp-server-http.ts b/src/mcp-server-http.ts index f14799b..02346cd 100644 --- a/src/mcp-server-http.ts +++ b/src/mcp-server-http.ts @@ -113,13 +113,17 @@ async function createMcpServer() { openWorldHint: false, }, }, - async ({ sectionId, recursive }) => { + async ({ sectionIds, recursive }) => { console.log( - `[TOOL] ${sectionContentToolName}: sectionId="${sectionId}" recursive=${recursive}`, + `[TOOL] ${sectionContentToolName}: sectionIds=[${sectionIds.map((id: string) => `"${id}"`).join(", ")}] recursive=${recursive}`, + ); + const result = await getSectionContentTool({ sectionIds, recursive }); + const totalContentLength = result.sections.reduce( + (sum: number, s: { content: string }) => sum + s.content.length, + 0, ); - const result = await getSectionContentTool({ sectionId, recursive }); console.log( - `[TOOL] ${sectionContentToolName}: ${result.content.length} chars, ${result.sectionCount} sections`, + `[TOOL] ${sectionContentToolName}: ${totalContentLength} chars, ${result.sections.length} sections`, ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], @@ -200,14 +204,14 @@ I'll use one of these orchestration patterns: PATTERN 1 - For "What happens when I run this code?" questions: - Use ask262Debug.startImportant() and ask262Debug.stopImportant() in the code to mark only important sections. - STEP 1: ask262_evaluate_in_engine262(code: markedCode) - - STEP 2: ask262_get_section_content(sectionId: importantSections[0], recursive: true) + - STEP 2: ask262_get_section_content(sectionIds: ["sec-1", "sec-2"], recursive: true) - Explain which spec sections were hit and why PATTERN 2 - For "How does X work?" questions (e.g., "${question}"): - Flow A: Generate a specific code example and follow Pattern 1 - Flow B: If no code example possible, search broadly: * STEP 1: ask262_search_spec_sections(query: relevant keywords from "${question}") - * STEP 2: ask262_get_section_content(sectionId: foundSectionId, recursive: true) + * STEP 2: ask262_get_section_content(sectionIds: ["sec-1", "sec-2"], recursive: true) I prefer Pattern 1 when possible as it provides exact spec sections through execution. diff --git a/src/mcp-server-stdio.ts b/src/mcp-server-stdio.ts index 702db46..97bcdc0 100644 --- a/src/mcp-server-stdio.ts +++ b/src/mcp-server-stdio.ts @@ -147,10 +147,10 @@ export async function main() { }, }, async ({ - sectionId, + sectionIds, recursive, }: GetSectionContentMCPInput): Promise => { - const result = await getSectionContentTool({ sectionId, recursive }); + const result = await getSectionContentTool({ sectionIds, recursive }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result, @@ -222,14 +222,14 @@ I'll use one of these orchestration patterns: PATTERN 1 - For "What happens when I run this code?" questions: - Use ask262Debug.startImportant() and ask262Debug.stopImportant() in the code to mark only important sections. - STEP 1: ask262_evaluate_in_engine262(code: markedCode) - - STEP 2: ask262_get_section_content(sectionId: importantSections[0], recursive: true) + - STEP 2: ask262_get_section_content(sectionIds: [importantSections[0]], recursive: true) - Explain which spec sections were hit and why PATTERN 2 - For "How does X work?" questions (e.g., "${question}"): - Flow A: Generate a specific code example and follow Pattern 1 - Flow B: If no code example possible, search broadly: * STEP 1: ask262_search_spec_sections(query: relevant keywords from "${question}") - * STEP 2: ask262_get_section_content(sectionId: foundSectionId, recursive: true) + * STEP 2: ask262_get_section_content(sectionIds: [foundSectionId], recursive: true) I prefer Pattern 1 when possible as it provides exact spec sections through execution. diff --git a/src/test/manual/test-mcp-server.ts b/src/test/manual/test-mcp-server.ts index bf75939..26adbb5 100644 --- a/src/test/manual/test-mcp-server.ts +++ b/src/test/manual/test-mcp-server.ts @@ -71,7 +71,7 @@ async function testMCPServer() { const contentResult = (await client.callTool({ name: "ask262_get_section_content", arguments: { - sectionId: "sec-array.prototype.map", + sectionIds: ["sec-array.prototype.map"], recursive: false, }, })) as GetSectionContentMCPOutput; @@ -79,10 +79,13 @@ async function testMCPServer() { throw new Error(`Get section failed: ${contentResult.content[0]?.text}`); } const contentData = contentResult.structuredContent; - console.log( - `Content length: ${contentData.content?.length ?? 0} characters`, - ); - console.log(`Sections visited: ${contentData.sectionCount ?? 0}`); + const totalContentLength = + contentData.sections?.reduce( + (sum: number, s: { content: string }) => sum + s.content.length, + 0, + ) ?? 0; + console.log(`Content length: ${totalContentLength} characters`); + console.log(`Sections visited: ${contentData.sections?.length ?? 0}`); console.log("✓ Section content retrieved (isError: false)\n"); // Test 4: Evaluate in engine262 - success case diff --git a/src/test/manual/test-nonexistent-sections.ts b/src/test/manual/test-nonexistent-sections.ts new file mode 100644 index 0000000..c005eb4 --- /dev/null +++ b/src/test/manual/test-nonexistent-sections.ts @@ -0,0 +1,70 @@ +#!/usr/bin/env bun +/** + * Test to verify non-existing section IDs return found: false + */ + +import * as lancedbSdk from "@lancedb/lancedb"; +import { createGetSectionContentTool } from "../../agent-tools/getSectionContent.js"; +import { STORAGE_DIR } from "../../constants.js"; + +async function main() { + console.log("Testing getSectionContent with non-existing section IDs...\n"); + + try { + const db = await lancedbSdk.connect(STORAGE_DIR); + const table = await db.openTable("spec_vectors"); + + const getSectionContentTool = createGetSectionContentTool(table); + + // Test with mix of existing and non-existing section IDs + const result = await getSectionContentTool({ + sectionIds: [ + "sec-non-existent-12345", // Should not exist + "sec-also-fake-99999", // Should not exist + ], + recursive: false, + }); + + console.log("=== RESULT ==="); + console.log(JSON.stringify(result, null, 2)); + console.log("\n=== VERIFICATION ==="); + + // Verify all sections are returned + if (result.sections.length !== 2) { + console.error(`❌ Expected 2 sections, got ${result.sections.length}`); + process.exit(1); + } + + // Verify non-existing sections have found: false + for (const section of result.sections) { + if (section.found !== false) { + console.error( + `❌ Section ${section.sectionId} should have found: false`, + ); + process.exit(1); + } + if (!section.error) { + console.error( + `❌ Section ${section.sectionId} should have error message`, + ); + process.exit(1); + } + if (section.content !== "") { + console.error( + `❌ Section ${section.sectionId} should have empty content`, + ); + process.exit(1); + } + console.log( + `✅ ${section.sectionId}: found=false, error="${section.error}"`, + ); + } + + console.log("\n✅ All non-existing sections correctly return found: false"); + } catch (error) { + console.error("❌ Test failed:", error); + process.exit(1); + } +} + +main().catch(console.error);