getSectionContent - allow multiple sectionids

This commit is contained in:
2026-04-20 18:32:59 +05:30
parent a26f90cef9
commit 0334c5836b
5 changed files with 159 additions and 30 deletions
+67 -15
View File
@@ -14,9 +14,18 @@ const sectionContentSchema = z.object({
partIndex: z.number().optional(), partIndex: z.number().optional(),
}); });
const getSectionContentOutputSchema = z.object({ const sectionDataSchema = z.object({
sectionId: z.string(),
content: 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 // #endregion
@@ -25,20 +34,21 @@ const getSectionContentOutputSchema = z.object({
export const toolMetadata = { export const toolMetadata = {
description: description:
"Retrieves all text chunks from a specific specification section by sectionid. " + "Retrieves all text chunks from one or more specification sections by section IDs. " +
"Supports recursive fetching - if recursive=true and the section has children, it will fetch all descendants. " + "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.", "Use this to get complete content when you see 'Subsection available' or 'partial section' references.",
args: { 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: recursive:
"If true, recursively fetches content from all child sections and their descendants. " + "If true, recursively fetches content from all child sections and their descendants. " +
"If false, only returns content from the specified section itself. " + "If false, only returns content from the specified sections themselves. " +
"Use false when you only need the specific section's content without subsections.", "Use false when you only need the specific sections' content without subsections.",
}, },
}; };
export const inputSchema = z.object({ 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), recursive: z.boolean().default(true).describe(toolMetadata.args.recursive),
}); });
@@ -69,11 +79,19 @@ export type GetSectionContentInput = z.infer<typeof inputSchema>;
*/ */
export function createGetSectionContentTool(table: Table) { export function createGetSectionContentTool(table: Table) {
return async ({ return async ({
sectionId, sectionIds,
recursive, recursive,
}: GetSectionContentInput): Promise<GetSectionContentOutput> => { }: GetSectionContentInput): Promise<GetSectionContentOutput> => {
const allDocs: string[] = []; const sectionsData = new Map<
const queue: string[] = [sectionId]; string,
{
content: string[];
title?: string;
partIndex?: number;
totalParts?: number;
}
>();
const queue: string[] = [...sectionIds];
const visited = new Set<string>(); const visited = new Set<string>();
while (queue.length > 0) { while (queue.length > 0) {
@@ -84,7 +102,7 @@ export function createGetSectionContentTool(table: Table) {
const results = await table const results = await table
.query() .query()
.where(`sectionid = '${currentId}'`) .where(`sectionid = '${currentId}'`)
.limit(100) .limit(10)
.toArray(); .toArray();
// Sort by partindex to maintain order (nulls last for single-part sections) // Sort by partindex to maintain order (nulls last for single-part sections)
@@ -99,10 +117,23 @@ export function createGetSectionContentTool(table: Table) {
text?: string; text?: string;
childrensectionids?: string[]; childrensectionids?: string[];
sectiontitle?: string; sectiontitle?: string;
partindex?: number;
totalparts?: number;
}; };
if (typedResult.text) { 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 // 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 { return {
content: allDocs.join("\n\n---\n\n"), sectionId: id,
sectionCount: visited.size, 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 {
sections,
}; };
}; };
} }
+10 -6
View File
@@ -113,13 +113,17 @@ async function createMcpServer() {
openWorldHint: false, openWorldHint: false,
}, },
}, },
async ({ sectionId, recursive }) => { async ({ sectionIds, recursive }) => {
console.log( 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( console.log(
`[TOOL] ${sectionContentToolName}: ${result.content.length} chars, ${result.sectionCount} sections`, `[TOOL] ${sectionContentToolName}: ${totalContentLength} chars, ${result.sections.length} sections`,
); );
return { return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }], 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: 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. - Use ask262Debug.startImportant() and ask262Debug.stopImportant() in the code to mark only important sections.
- STEP 1: ask262_evaluate_in_engine262(code: markedCode) - 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 - Explain which spec sections were hit and why
PATTERN 2 - For "How does X work?" questions (e.g., "${question}"): PATTERN 2 - For "How does X work?" questions (e.g., "${question}"):
- Flow A: Generate a specific code example and follow Pattern 1 - Flow A: Generate a specific code example and follow Pattern 1
- Flow B: If no code example possible, search broadly: - Flow B: If no code example possible, search broadly:
* STEP 1: ask262_search_spec_sections(query: relevant keywords from "${question}") * 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. I prefer Pattern 1 when possible as it provides exact spec sections through execution.
+4 -4
View File
@@ -147,10 +147,10 @@ export async function main() {
}, },
}, },
async ({ async ({
sectionId, sectionIds,
recursive, recursive,
}: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => { }: GetSectionContentMCPInput): Promise<GetSectionContentMCPOutput> => {
const result = await getSectionContentTool({ sectionId, recursive }); const result = await getSectionContentTool({ sectionIds, recursive });
return { return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }], content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
structuredContent: result, 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: 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. - Use ask262Debug.startImportant() and ask262Debug.stopImportant() in the code to mark only important sections.
- STEP 1: ask262_evaluate_in_engine262(code: markedCode) - 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 - Explain which spec sections were hit and why
PATTERN 2 - For "How does X work?" questions (e.g., "${question}"): PATTERN 2 - For "How does X work?" questions (e.g., "${question}"):
- Flow A: Generate a specific code example and follow Pattern 1 - Flow A: Generate a specific code example and follow Pattern 1
- Flow B: If no code example possible, search broadly: - Flow B: If no code example possible, search broadly:
* STEP 1: ask262_search_spec_sections(query: relevant keywords from "${question}") * 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. I prefer Pattern 1 when possible as it provides exact spec sections through execution.
+8 -5
View File
@@ -71,7 +71,7 @@ async function testMCPServer() {
const contentResult = (await client.callTool({ const contentResult = (await client.callTool({
name: "ask262_get_section_content", name: "ask262_get_section_content",
arguments: { arguments: {
sectionId: "sec-array.prototype.map", sectionIds: ["sec-array.prototype.map"],
recursive: false, recursive: false,
}, },
})) as GetSectionContentMCPOutput; })) as GetSectionContentMCPOutput;
@@ -79,10 +79,13 @@ async function testMCPServer() {
throw new Error(`Get section failed: ${contentResult.content[0]?.text}`); throw new Error(`Get section failed: ${contentResult.content[0]?.text}`);
} }
const contentData = contentResult.structuredContent; const contentData = contentResult.structuredContent;
console.log( const totalContentLength =
`Content length: ${contentData.content?.length ?? 0} characters`, contentData.sections?.reduce(
); (sum: number, s: { content: string }) => sum + s.content.length,
console.log(`Sections visited: ${contentData.sectionCount ?? 0}`); 0,
) ?? 0;
console.log(`Content length: ${totalContentLength} characters`);
console.log(`Sections visited: ${contentData.sections?.length ?? 0}`);
console.log("✓ Section content retrieved (isError: false)\n"); console.log("✓ Section content retrieved (isError: false)\n");
// Test 4: Evaluate in engine262 - success case // Test 4: Evaluate in engine262 - success case
@@ -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);