Skip to content

Commit a652454

Browse files
feat: add get_doc_content tool for retrieving indexed document content (#72)
* feat: add get_doc_content tool for retrieving indexed document content (#70) Add MCP tool that fetches .md document content from GitHub repositories via the contents API. Complements search_issues type:"doc" by letting users read the actual file content after discovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: decode base64 content via Uint8Array for UTF-8 safety atob() returns Latin-1 which corrupts multi-byte characters (e.g. Japanese). Decode through Uint8Array + TextDecoder to preserve UTF-8 correctly. Refs #70 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: liplus-lin-lay <liplus-lin-lay@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c604a4c commit a652454

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

‎src/mcp.ts‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Tools:
55
* search_issues — semantic + structured search via Vectorize + Workers AI
66
* get_issue_context — aggregated issue view with related PRs, branch, CI
7+
* get_doc_content — retrieve .md document content from a repository
78
* list_recent_activity — recent changes across tracked repositories
89
*
910
* Extends McpAgent from "agents/mcp" (same pattern as github-webhook-mcp).
@@ -546,6 +547,120 @@ export class RagMcpAgent extends McpAgent<Env, unknown, McpProps> {
546547
},
547548
);
548549

550+
// ── get_doc_content ─────────────────────────────────────────
551+
this.server.tool(
552+
"get_doc_content",
553+
"Retrieve the content of a document file (.md) from a GitHub repository. " +
554+
"Use this to read documents found via search_issues with type: \"doc\". " +
555+
"Returns the raw file content fetched from the repository.",
556+
{
557+
repo: z
558+
.string()
559+
.describe("Repository (owner/repo)"),
560+
path: z
561+
.string()
562+
.describe(
563+
"File path in the repository (e.g. \"docs/0-requirements.md\")",
564+
),
565+
ref: z
566+
.string()
567+
.optional()
568+
.describe(
569+
"Git ref (branch, tag, or commit SHA) to fetch from. Defaults to the repository's default branch.",
570+
),
571+
},
572+
async ({ repo, path, ref }) => {
573+
const token = this.getGitHubToken();
574+
const headers = githubHeaders(token);
575+
576+
const url = new URL(`${GITHUB_API}/repos/${repo}/contents/${path}`);
577+
if (ref) {
578+
url.searchParams.set("ref", ref);
579+
}
580+
581+
let response: Response;
582+
try {
583+
response = await fetch(url.toString(), { headers });
584+
} catch (err) {
585+
const message =
586+
err instanceof Error ? err.message : String(err);
587+
console.error(
588+
`get_doc_content: network error fetching ${repo}/${path}: ${message}`,
589+
);
590+
return {
591+
content: [
592+
{
593+
type: "text" as const,
594+
text: `Failed to fetch document: network error — ${message}`,
595+
},
596+
],
597+
isError: true,
598+
};
599+
}
600+
601+
if (!response.ok) {
602+
const body = await response.text().catch(() => "");
603+
console.error(
604+
`get_doc_content: GitHub API returned ${response.status} for ${repo}/${path}: ${body}`,
605+
);
606+
return {
607+
content: [
608+
{
609+
type: "text" as const,
610+
text: `Failed to fetch document: HTTP ${response.status} — ${body}`,
611+
},
612+
],
613+
isError: true,
614+
};
615+
}
616+
617+
const data = (await response.json()) as {
618+
content?: string;
619+
encoding?: string;
620+
name?: string;
621+
path?: string;
622+
size?: number;
623+
sha?: string;
624+
html_url?: string;
625+
};
626+
627+
if (!data.content) {
628+
console.error(
629+
`get_doc_content: no content field in response for ${repo}/${path}`,
630+
);
631+
return {
632+
content: [
633+
{
634+
type: "text" as const,
635+
text: "Document response did not contain file content. The path may point to a directory or an unsupported file type.",
636+
},
637+
],
638+
isError: true,
639+
};
640+
}
641+
642+
// GitHub returns base64-encoded content; decode via Uint8Array for UTF-8 safety
643+
const binary = atob(data.content.replace(/\n/g, ""));
644+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
645+
const decoded = new TextDecoder().decode(bytes);
646+
647+
const result = {
648+
repo,
649+
path: data.path ?? path,
650+
sha: data.sha ?? "",
651+
size: data.size ?? 0,
652+
url: data.html_url ?? `https://github.com/${repo}/blob/main/${path}`,
653+
content: decoded,
654+
};
655+
656+
return {
657+
content: [
658+
{ type: "text" as const, text: JSON.stringify(result, null, 2) },
659+
],
660+
};
661+
},
662+
);
663+
549664
// ── list_recent_activity ───────────────────────────────────
550665
this.server.tool(
551666
"list_recent_activity",

0 commit comments

Comments
 (0)