
Docs.pudu
Pudu86 lines3.7 KB
1/** @App.Tools.Docs.Handler — documentation search and reading tools */2module PuduLangMcp.App.Tools.Docs34import Std.List as List5import Std.Option as Option6import Std.Text as Text7import PuduLangMcp.App.Context as Context8import PuduLangMcp.Constants.Server as Server9import PuduLangMcp.Domain.Catalog.Arguments as Arguments10import PuduLangMcp.Domain.Docs.Chapter as Chapter11import PuduLangMcp.Domain.Docs.Search as Search12import PuduLangMcp.Errors.ToolError as ToolError13import PuduLangMcp.Utils.TextBounds as TextBounds141516export const GROUPS: Array[Str] = ["docs", "pages", "releases", "examples"]171819const DEFAULT_GROUP: Str = "docs"202122const SNIPPET_WIDTH: Int = 240232425export fn search(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {26 let query = Option.unwrapOr(Arguments.text(args, "query"), "")27 let group = groupOf(args) ?28 let limit = Arguments.integerOr(args, "limit", Server.DEFAULT_SEARCH_LIMIT)29 let sections = match group {30 case Some(wanted) => context.sections.filter(|held: Chapter.Section| held.group == wanted)31 case None => context.sections32 }33 let hits = Search.search(§ions, query, limit)34 if hits.isEmpty() {35 return Ok("No documentation matches '" + query + "'. Try other words, or pudu_reference_search for a library name.")36 }37 let first = Option.unwrapOr(List.first(&Search.terms(query)), query)38 var lines: Array[Str] = []39 var number = 140 for hit in hits {41 let held = hit.section42 lines = lines.push(show(number) + ". " + held.group + "/" + held.chapter + "#" + held.anchor + " — " + held.chapterTitle + " › " + held.heading)43 lines = lines.push(" " + TextBounds.snippet(held.text, first, SNIPPET_WIDTH))44 number = number + 145 }46 Ok(lines.join("\n") + "\n\nRead one with pudu_docs_read using its slug and, for a section, its anchor.")47}484950export fn read(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {51 let group = Option.unwrapOr(groupOf(args) ?, DEFAULT_GROUP)52 let slug = Option.unwrapOr(Arguments.text(args, "slug"), "").trim()53 let chapter = match Chapter.find(&context.chapters, group, slug) {54 case Some(found) => found55 case None => {56 let known = Chapter.inGroup(&context.chapters, group).map(|held: Chapter.Chapter| held.slug)57 return Err(ToolError.NotFound("Document '" + group + "/" + slug + "' (" + group + " has: " + known.join(", ") + ")"))58 }59 }60 match Arguments.text(args, "section") {61 case Some(wanted) => {62 if Text.isBlank(wanted) { return Ok(TextBounds.bounded(chapter.markdown, Server.DOC_CHARS)) }63 match Chapter.sectionIn(&chapter, wanted) {64 case Some(held) => Ok(TextBounds.bounded("## " + held.heading + "\n\n" + held.text, Server.DOC_CHARS))65 case None => {66 let anchors = Chapter.sectionsOf(&chapter).map(|held: Chapter.Section| held.anchor)67 Err(ToolError.NotFound("Section '" + wanted + "' of " + group + "/" + slug + " (sections: " + anchors.join(", ") + ")"))68 }69 }70 }71 case None => Ok(TextBounds.bounded(chapter.markdown, Server.DOC_CHARS))72 }73}747576fn groupOf(args: &Arguments.Args) -> Result[Option[Str], ToolError.ToolFailure] {77 match Arguments.text(args, "group") {78 case None => Ok(None)79 case Some(given) => {80 let wanted = given.trim()81 if wanted.isEmpty() { return Ok(None) }82 if GROUPS.contains(wanted) { Ok(Some(wanted)) } else { Err(ToolError.OutOfRange("group", "one of " + GROUPS.join(", "))) }83 }84 }85}86