@finchagentic/mcp 4.6.0 → 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +7 -7
  2. package/package.json +5 -6
  3. package/dist/_http-cache.js +0 -96
  4. package/dist/agent-loop.js +0 -301
  5. package/dist/annotations.js +0 -122
  6. package/dist/cli.js +0 -1391
  7. package/dist/clink-input.js +0 -15
  8. package/dist/config.js +0 -132
  9. package/dist/convex.js +0 -175
  10. package/dist/dex-pair.js +0 -54
  11. package/dist/enrichment-router.js +0 -315
  12. package/dist/index.js +0 -258
  13. package/dist/llm.js +0 -298
  14. package/dist/local-memory-file.js +0 -147
  15. package/dist/local-memory.js +0 -135
  16. package/dist/local-vault.js +0 -454
  17. package/dist/output-schemas.js +0 -605
  18. package/dist/project.js +0 -36
  19. package/dist/prompts.js +0 -111
  20. package/dist/public-url.js +0 -107
  21. package/dist/resources.js +0 -111
  22. package/dist/server.js +0 -322
  23. package/dist/signal-gate.js +0 -57
  24. package/dist/token-decimals.js +0 -26
  25. package/dist/token-gate.js +0 -88
  26. package/dist/tool-filter.js +0 -53
  27. package/dist/tools/_solidity-scan.js +0 -313
  28. package/dist/tools/agents.js +0 -441
  29. package/dist/tools/automation.js +0 -354
  30. package/dist/tools/base-mcp.js +0 -466
  31. package/dist/tools/base.js +0 -283
  32. package/dist/tools/chronicle.js +0 -268
  33. package/dist/tools/coder.js +0 -94
  34. package/dist/tools/deep-research.js +0 -1421
  35. package/dist/tools/defi.js +0 -292
  36. package/dist/tools/equity.js +0 -372
  37. package/dist/tools/events.js +0 -182
  38. package/dist/tools/github.js +0 -564
  39. package/dist/tools/insider.js +0 -264
  40. package/dist/tools/insight.js +0 -630
  41. package/dist/tools/market.js +0 -555
  42. package/dist/tools/memory.js +0 -1044
  43. package/dist/tools/miroshark.js +0 -350
  44. package/dist/tools/monitor.js +0 -319
  45. package/dist/tools/os.js +0 -236
  46. package/dist/tools/packets.js +0 -296
  47. package/dist/tools/research-chain.js +0 -226
  48. package/dist/tools/research-compare.js +0 -280
  49. package/dist/tools/research.js +0 -188
  50. package/dist/tools/rh-bridge.js +0 -148
  51. package/dist/tools/rh-mcp.js +0 -1448
  52. package/dist/tools/rh-orders.js +0 -556
  53. package/dist/tools/scanner.js +0 -564
  54. package/dist/tools/stake.js +0 -369
  55. package/dist/tools/vault.js +0 -1020
  56. package/dist/tools/wallet.js +0 -200
  57. package/dist/types.js +0 -2
  58. package/dist/wallet.js +0 -372
@@ -1,564 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.GITHUB_TOOLS = void 0;
37
- exports.buildRepoList = buildRepoList;
38
- exports.buildPrList = buildPrList;
39
- exports.buildPrDetail = buildPrDetail;
40
- exports.buildIssueList = buildIssueList;
41
- exports.buildIssueDetail = buildIssueDetail;
42
- exports.buildCommitList = buildCommitList;
43
- exports.buildCodeSearch = buildCodeSearch;
44
- exports.handleGithubTool = handleGithubTool;
45
- const fs = __importStar(require("fs"));
46
- const path = __importStar(require("path"));
47
- const GH_BASE = "https://api.github.com";
48
- // Was hardcoded "finch-mcp/3.28.0" - stale the moment package.json's version
49
- // moved on (already 4.4.0 locally as of this fix), unlike server.ts/cli.ts/
50
- // index.ts which all read it from package.json at runtime per this repo's
51
- // own stated policy (see CLAUDE.md's mcp-server note). __dirname here is
52
- // dist/tools/ once built, so two levels up reaches the package root - one
53
- // level deeper than server.ts's own copy of this pattern (dist/).
54
- const PKG_VERSION = (() => {
55
- try {
56
- const raw = fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8");
57
- return JSON.parse(raw).version ?? "unknown";
58
- }
59
- catch {
60
- return "unknown";
61
- }
62
- })();
63
- function ghHeaders() {
64
- const token = process.env.GITHUB_TOKEN;
65
- const h = {
66
- Accept: "application/vnd.github.v3+json",
67
- "X-GitHub-Api-Version": "2022-11-28",
68
- "User-Agent": `finch-mcp/${PKG_VERSION}`,
69
- };
70
- if (token)
71
- h.Authorization = `Bearer ${token}`;
72
- return h;
73
- }
74
- async function gh(path) {
75
- const res = await fetch(`${GH_BASE}${path}`, {
76
- headers: ghHeaders(),
77
- signal: AbortSignal.timeout(12000),
78
- });
79
- if (!res.ok) {
80
- const err = await res.json().catch(() => ({}));
81
- throw new Error(`GitHub ${res.status}: ${err.message ?? res.statusText}`);
82
- }
83
- return res.json();
84
- }
85
- function md(text, structuredContent) {
86
- return structuredContent
87
- ? { content: [{ type: "text", text }], structuredContent }
88
- : { content: [{ type: "text", text }] };
89
- }
90
- // ── Structured output builders (MCP outputSchema payloads in output-schemas.ts) ──
91
- function buildRepoList(username, repos) {
92
- return {
93
- username: username ?? null,
94
- count: repos.length,
95
- repos: repos.map((r) => ({
96
- fullName: r.full_name,
97
- description: r.description ?? null,
98
- language: r.language ?? null,
99
- stars: r.stargazers_count ?? 0,
100
- forks: r.forks_count ?? 0,
101
- private: !!r.private,
102
- updatedAt: r.updated_at ?? null,
103
- url: r.html_url,
104
- })),
105
- };
106
- }
107
- function buildPrList(owner, repo, state, prs) {
108
- return {
109
- owner, repo, state,
110
- count: prs.length,
111
- prs: prs.map((p) => ({
112
- number: p.number,
113
- title: p.title,
114
- author: p.user?.login ?? null,
115
- draft: !!p.draft,
116
- head: p.head?.ref ?? null,
117
- base: p.base?.ref ?? null,
118
- additions: p.additions ?? null,
119
- deletions: p.deletions ?? null,
120
- url: p.html_url,
121
- updatedAt: p.updated_at ?? null,
122
- })),
123
- };
124
- }
125
- function buildPrDetail(pr, files, reviews, comments) {
126
- return {
127
- number: pr.number,
128
- title: pr.title,
129
- state: pr.state,
130
- draft: !!pr.draft,
131
- author: pr.user?.login ?? null,
132
- head: pr.head?.ref ?? null,
133
- base: pr.base?.ref ?? null,
134
- additions: pr.additions ?? 0,
135
- deletions: pr.deletions ?? 0,
136
- changedFiles: pr.changed_files ?? 0,
137
- url: pr.html_url,
138
- files: (files ?? []).slice(0, 50).map((f) => ({
139
- filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions,
140
- })),
141
- reviewCount: (reviews ?? []).length,
142
- commentCount: (comments ?? []).length,
143
- };
144
- }
145
- function buildIssueList(owner, repo, state, issues) {
146
- return {
147
- owner, repo, state,
148
- count: issues.length,
149
- issues: issues.map((i) => ({
150
- number: i.number,
151
- title: i.title,
152
- author: i.user?.login ?? null,
153
- labels: (i.labels ?? []).map((l) => l.name),
154
- comments: i.comments ?? 0,
155
- url: i.html_url,
156
- updatedAt: i.updated_at ?? null,
157
- })),
158
- };
159
- }
160
- function buildIssueDetail(issue, comments) {
161
- return {
162
- number: issue.number,
163
- title: issue.title,
164
- state: issue.state,
165
- author: issue.user?.login ?? null,
166
- labels: (issue.labels ?? []).map((l) => l.name),
167
- assignees: (issue.assignees ?? []).map((a) => a.login),
168
- commentCount: (comments ?? []).length,
169
- url: issue.html_url,
170
- createdAt: issue.created_at ?? null,
171
- };
172
- }
173
- function buildCommitList(owner, repo, branch, path, commits) {
174
- return {
175
- owner, repo,
176
- branch: branch ?? null,
177
- path: path ?? null,
178
- count: commits.length,
179
- commits: commits.map((c) => ({
180
- sha: c.sha,
181
- message: c.commit?.message?.split("\n")[0] ?? null,
182
- author: c.commit?.author?.name ?? c.author?.login ?? null,
183
- date: c.commit?.author?.date ?? null,
184
- url: c.html_url,
185
- })),
186
- };
187
- }
188
- function buildCodeSearch(query, totalCount, items) {
189
- return {
190
- query,
191
- totalCount: totalCount ?? items.length,
192
- count: items.length,
193
- results: items.map((it) => ({ path: it.path, repo: it.repository?.full_name ?? null, url: it.html_url })),
194
- };
195
- }
196
- function fmtDate(iso) {
197
- if (!iso)
198
- return "—";
199
- return new Date(iso).toISOString().slice(0, 10);
200
- }
201
- function langExt(filename) {
202
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
203
- const map = {
204
- ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript",
205
- py: "python", rb: "ruby", go: "go", rs: "rust", java: "java",
206
- cs: "csharp", cpp: "cpp", c: "c", sh: "bash", yaml: "yaml", yml: "yaml",
207
- json: "json", toml: "toml", md: "markdown", html: "html", css: "css", sql: "sql",
208
- };
209
- return map[ext] ?? ext;
210
- }
211
- exports.GITHUB_TOOLS = [
212
- {
213
- name: "github_list_repos",
214
- description: "List GitHub repositories for a user or org. Leave username empty to list your own repos (requires GITHUB_TOKEN). " +
215
- "Returns name, description, language, stars, forks, last updated.",
216
- inputSchema: {
217
- type: "object",
218
- properties: {
219
- username: { type: "string", description: "GitHub username or org. Empty = your own repos (needs GITHUB_TOKEN)." },
220
- sort: { type: "string", enum: ["updated", "created", "pushed", "full_name"], description: "Sort field (default: updated)" },
221
- per_page: { type: "number", description: "Max results (default 20, max 100)" },
222
- },
223
- },
224
- },
225
- {
226
- name: "github_list_prs",
227
- description: "List pull requests for a GitHub repository. Returns PR number, title, author, state, branch, additions/deletions.",
228
- inputSchema: {
229
- type: "object",
230
- properties: {
231
- owner: { type: "string", description: "Repo owner (user or org)" },
232
- repo: { type: "string", description: "Repository name" },
233
- state: { type: "string", enum: ["open", "closed", "all"], description: "PR state (default: open)" },
234
- per_page: { type: "number", description: "Max results (default 15, max 100)" },
235
- },
236
- required: ["owner", "repo"],
237
- },
238
- },
239
- {
240
- name: "github_get_pr",
241
- description: "Get full details of a pull request - title, body, diff summary, changed files, reviews, and comments. " +
242
- "Use to understand what a PR does before reviewing or merging.",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- owner: { type: "string", description: "Repo owner" },
247
- repo: { type: "string", description: "Repository name" },
248
- pr_number: { type: "number", description: "Pull request number" },
249
- },
250
- required: ["owner", "repo", "pr_number"],
251
- },
252
- },
253
- {
254
- name: "github_list_issues",
255
- description: "List issues for a GitHub repository. Returns issue number, title, author, labels, comment count.",
256
- inputSchema: {
257
- type: "object",
258
- properties: {
259
- owner: { type: "string", description: "Repo owner" },
260
- repo: { type: "string", description: "Repository name" },
261
- state: { type: "string", enum: ["open", "closed", "all"], description: "Issue state (default: open)" },
262
- labels: { type: "string", description: "Comma-separated label names to filter by" },
263
- per_page: { type: "number", description: "Max results (default 15, max 100)" },
264
- },
265
- required: ["owner", "repo"],
266
- },
267
- },
268
- {
269
- name: "github_get_issue",
270
- description: "Get a GitHub issue with full body and all comments. Use to understand a bug or feature request in depth.",
271
- inputSchema: {
272
- type: "object",
273
- properties: {
274
- owner: { type: "string", description: "Repo owner" },
275
- repo: { type: "string", description: "Repository name" },
276
- issue_number: { type: "number", description: "Issue number" },
277
- },
278
- required: ["owner", "repo", "issue_number"],
279
- },
280
- },
281
- {
282
- name: "github_get_file",
283
- description: "Read a file from a GitHub repository. Returns decoded content (up to 10k chars). Use for reading code, configs, READMEs.",
284
- inputSchema: {
285
- type: "object",
286
- properties: {
287
- owner: { type: "string", description: "Repo owner" },
288
- repo: { type: "string", description: "Repository name" },
289
- path: { type: "string", description: "File path in the repo (e.g. src/index.ts, README.md)" },
290
- ref: { type: "string", description: "Branch, tag, or commit SHA (default: main branch)" },
291
- },
292
- required: ["owner", "repo", "path"],
293
- },
294
- },
295
- {
296
- name: "github_get_commits",
297
- description: "Get recent commits for a repo, branch, or specific file. Returns SHA, message, author, date.",
298
- inputSchema: {
299
- type: "object",
300
- properties: {
301
- owner: { type: "string", description: "Repo owner" },
302
- repo: { type: "string", description: "Repository name" },
303
- branch: { type: "string", description: "Branch name (default: repo default branch)" },
304
- path: { type: "string", description: "Filter to commits touching a specific file path" },
305
- per_page: { type: "number", description: "Number of commits (default 15, max 100)" },
306
- },
307
- required: ["owner", "repo"],
308
- },
309
- },
310
- {
311
- name: "github_search_code",
312
- description: "Search code on GitHub. Supports qualifiers: repo:owner/repo, language:typescript, path:src/, filename:package.json, etc. " +
313
- "Requires GITHUB_TOKEN for best results.",
314
- inputSchema: {
315
- type: "object",
316
- properties: {
317
- query: { type: "string", description: "Search query with optional qualifiers (e.g. 'useState repo:facebook/react language:typescript')" },
318
- per_page: { type: "number", description: "Max results (default 10, max 30)" },
319
- },
320
- required: ["query"],
321
- },
322
- },
323
- ];
324
- async function handleGithubTool(name, args) {
325
- const a = (args ?? {});
326
- switch (name) {
327
- case "github_list_repos": {
328
- const sort = a.sort ?? "updated";
329
- const per_page = Math.min(a.per_page ?? 20, 100);
330
- const ghPath = a.username
331
- ? `/users/${encodeURIComponent(a.username)}/repos?sort=${sort}&per_page=${per_page}`
332
- : `/user/repos?type=owner&sort=${sort}&per_page=${per_page}`;
333
- const repos = await gh(ghPath);
334
- const who = a.username ?? "you";
335
- const header = `## GitHub Repos — ${who} (${repos.length})\n`;
336
- const rows = repos.map(r => {
337
- const vis = r.private ? "🔒" : "📂";
338
- const lang = r.language ? ` · ${r.language}` : "";
339
- const desc = r.description ? `\n ${r.description.slice(0, 100)}` : "";
340
- return `${vis} **[${r.full_name}](${r.html_url})** ⭐${r.stargazers_count} 🍴${r.forks_count}${lang} · ${fmtDate(r.updated_at)}${desc}`;
341
- });
342
- return md([header, ...rows].join("\n"), buildRepoList(a.username, repos));
343
- }
344
- case "github_list_prs": {
345
- const { owner, repo, state = "open" } = a;
346
- const per_page = Math.min(a.per_page ?? 15, 100);
347
- const prs = await gh(`/repos/${owner}/${repo}/pulls?state=${state}&per_page=${per_page}`);
348
- if (!prs.length)
349
- return md(`No ${state} PRs in **${owner}/${repo}**.`, buildPrList(owner, repo, state, []));
350
- const header = `## PRs — ${owner}/${repo} (${state}, ${prs.length})\n`;
351
- const rows = prs.map(p => {
352
- const draft = p.draft ? " `draft`" : "";
353
- const changes = (p.additions != null)
354
- ? ` · \`+${p.additions}/-${p.deletions}\``
355
- : "";
356
- const body = p.body ? `\n ${p.body.slice(0, 120).replace(/\n/g, " ")}…` : "";
357
- return `**#${p.number}** [${p.title}](${p.html_url})${draft}\n @${p.user?.login} · \`${p.head?.ref}\` → \`${p.base?.ref}\`${changes} · ${fmtDate(p.updated_at)}${body}`;
358
- });
359
- return md([header, ...rows].join("\n\n"), buildPrList(owner, repo, state, prs));
360
- }
361
- case "github_get_pr": {
362
- const { owner, repo, pr_number } = a;
363
- const [pr, files, reviews, comments] = await Promise.all([
364
- gh(`/repos/${owner}/${repo}/pulls/${pr_number}`),
365
- gh(`/repos/${owner}/${repo}/pulls/${pr_number}/files?per_page=50`),
366
- gh(`/repos/${owner}/${repo}/pulls/${pr_number}/reviews`),
367
- gh(`/repos/${owner}/${repo}/issues/${pr_number}/comments`),
368
- ]);
369
- const stateEmoji = pr.state === "open" ? (pr.draft ? "🟡 Draft" : "🟢 Open") : "🟣 Merged";
370
- const lines = [
371
- `## PR #${pr.number} — ${pr.title}`,
372
- `${stateEmoji} · @${pr.user?.login} · \`${pr.head?.ref}\` → \`${pr.base?.ref}\` · ${fmtDate(pr.updated_at)}`,
373
- `\`+${pr.additions ?? 0}/-${pr.deletions ?? 0}\` across ${pr.changed_files ?? 0} files · [View on GitHub](${pr.html_url})`,
374
- ];
375
- if (pr.body) {
376
- lines.push("", "### Description", pr.body.slice(0, 1500));
377
- }
378
- if (files?.length) {
379
- lines.push("", "### Changed Files");
380
- for (const f of files.slice(0, 25)) {
381
- const s = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
382
- lines.push(`\`${s}\` \`${f.filename}\` (+${f.additions}/-${f.deletions})`);
383
- if (f.patch)
384
- lines.push(`\`\`\`diff\n${f.patch.slice(0, 800)}\n\`\`\``);
385
- }
386
- if (files.length > 25)
387
- lines.push(`_…and ${files.length - 25} more files_`);
388
- }
389
- if (reviews?.length) {
390
- lines.push("", "### Reviews");
391
- for (const r of reviews) {
392
- if (!r.body && r.state === "COMMENTED")
393
- continue;
394
- lines.push(`**@${r.user?.login}** ${r.state} · ${fmtDate(r.submitted_at)}${r.body ? `\n> ${r.body.slice(0, 300)}` : ""}`);
395
- }
396
- }
397
- if (comments?.length) {
398
- lines.push("", "### Comments");
399
- for (const c of comments.slice(0, 8)) {
400
- lines.push(`**@${c.user?.login}** · ${fmtDate(c.created_at)}\n> ${c.body?.slice(0, 400)}`);
401
- }
402
- }
403
- return md(lines.join("\n"), buildPrDetail(pr, files, reviews, comments));
404
- }
405
- case "github_list_issues": {
406
- const { owner, repo, state = "open", labels } = a;
407
- const per_page = Math.min(a.per_page ?? 15, 100);
408
- let issuePath = `/repos/${owner}/${repo}/issues?state=${state}&per_page=${per_page}`;
409
- if (labels)
410
- issuePath += `&labels=${encodeURIComponent(labels)}`;
411
- const all = await gh(issuePath);
412
- const issues = all.filter((i) => !i.pull_request);
413
- if (!issues.length)
414
- return md(`No ${state} issues in **${owner}/${repo}**${labels ? ` with labels: ${labels}` : ""}.`, buildIssueList(owner, repo, state, []));
415
- const header = `## Issues — ${owner}/${repo} (${state}, ${issues.length})\n`;
416
- const rows = issues.map((i) => {
417
- const lbls = i.labels?.length ? ` · ${i.labels.map((l) => `\`${l.name}\``).join(" ")}` : "";
418
- const cmts = i.comments ? ` · 💬${i.comments}` : "";
419
- const body = i.body ? `\n ${i.body.slice(0, 100).replace(/\n/g, " ")}…` : "";
420
- return `**#${i.number}** [${i.title}](${i.html_url})\n @${i.user?.login}${lbls}${cmts} · ${fmtDate(i.updated_at)}${body}`;
421
- });
422
- return md([header, ...rows].join("\n\n"), buildIssueList(owner, repo, state, issues));
423
- }
424
- case "github_get_issue": {
425
- const { owner, repo, issue_number } = a;
426
- const [issue, comments] = await Promise.all([
427
- gh(`/repos/${owner}/${repo}/issues/${issue_number}`),
428
- gh(`/repos/${owner}/${repo}/issues/${issue_number}/comments?per_page=20`),
429
- ]);
430
- const lbls = issue.labels?.length ? issue.labels.map((l) => `\`${l.name}\``).join(" ") : "";
431
- const assignees = issue.assignees?.length ? `Assigned: @${issue.assignees.map((a) => a.login).join(", @")}` : "";
432
- const lines = [
433
- `## Issue #${issue.number} — ${issue.title}`,
434
- `${issue.state === "open" ? "🟢 Open" : "🔴 Closed"} · @${issue.user?.login} · ${fmtDate(issue.created_at)} · [View on GitHub](${issue.html_url})`,
435
- ];
436
- if (lbls)
437
- lines.push(lbls);
438
- if (assignees)
439
- lines.push(assignees);
440
- if (issue.body)
441
- lines.push("", issue.body.slice(0, 3000));
442
- if (comments?.length) {
443
- lines.push("", `---`, "", `### Comments (${comments.length})`);
444
- for (const c of comments) {
445
- lines.push(`**@${c.user?.login}** · ${fmtDate(c.created_at)}`);
446
- lines.push(c.body?.slice(0, 1000) ?? "");
447
- lines.push("");
448
- }
449
- }
450
- return md(lines.join("\n"), buildIssueDetail(issue, comments));
451
- }
452
- case "github_get_file": {
453
- const { owner, repo, path: filePath, ref } = a;
454
- let fileUrl = `/repos/${owner}/${repo}/contents/${filePath}`;
455
- if (ref)
456
- fileUrl += `?ref=${encodeURIComponent(ref)}`;
457
- const data = await gh(fileUrl);
458
- if (data.type !== "file") {
459
- return md(`Not a file — got type \`${data.type}\`. Specify a full file path, not a directory.`, { owner, repo, path: filePath, isFile: false });
460
- }
461
- const content = Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf-8");
462
- const truncated = content.length > 10000;
463
- const lang = langExt(filePath);
464
- const lines = [
465
- `## \`${data.path}\` — ${owner}/${repo}${ref ? ` @ ${ref}` : ""}`,
466
- `Size: ${(data.size / 1024).toFixed(1)}KB · SHA: \`${data.sha?.slice(0, 8)}\` · [View on GitHub](${data.html_url})`,
467
- "",
468
- `\`\`\`${lang}`,
469
- content.slice(0, 10000),
470
- "```",
471
- ];
472
- if (truncated)
473
- lines.push(`\n_File truncated at 10,000 chars (${(content.length / 1024).toFixed(1)}KB total)._`);
474
- return md(lines.join("\n"), {
475
- owner, repo, path: data.path, ref: ref ?? null, isFile: true,
476
- sizeBytes: data.size ?? null, sha: data.sha ?? null, url: data.html_url ?? null,
477
- language: lang, truncated, content: content.slice(0, 10000),
478
- });
479
- }
480
- case "github_get_commits": {
481
- const { owner, repo, branch, path: filePath } = a;
482
- const per_page = Math.min(a.per_page ?? 15, 100);
483
- let commitUrl = `/repos/${owner}/${repo}/commits?per_page=${per_page}`;
484
- if (branch)
485
- commitUrl += `&sha=${encodeURIComponent(branch)}`;
486
- if (filePath)
487
- commitUrl += `&path=${encodeURIComponent(filePath)}`;
488
- const commits = await gh(commitUrl);
489
- if (!commits.length)
490
- return md(`No commits found in **${owner}/${repo}**${branch ? ` @ ${branch}` : ""}${filePath ? ` for \`${filePath}\`` : ""}.`, buildCommitList(owner, repo, branch, filePath, []));
491
- const filter = filePath ? ` · \`${filePath}\`` : "";
492
- const header = `## Commits — ${owner}/${repo}${branch ? ` @ ${branch}` : ""}${filter} (${commits.length})\n`;
493
- const rows = commits.map(c => {
494
- const msg = c.commit?.message?.split("\n")[0].slice(0, 100) ?? "(no message)";
495
- const author = c.commit?.author?.name ?? c.author?.login ?? "unknown";
496
- const sha = c.sha?.slice(0, 7);
497
- return `\`${sha}\` **${msg}**\n @${author} · ${fmtDate(c.commit?.author?.date)} · [↗](${c.html_url})`;
498
- });
499
- return md([header, ...rows].join("\n\n"), buildCommitList(owner, repo, branch, filePath, commits));
500
- }
501
- case "github_search_code": {
502
- // GitHub's code-search endpoint requires authentication. Without
503
- // GITHUB_TOKEN it returns 422 with a non-obvious message. Detect that
504
- // case upfront and give the user a concrete setup path instead of an
505
- // opaque error.
506
- if (!process.env.GITHUB_TOKEN) {
507
- return {
508
- content: [{
509
- type: "text",
510
- text: [
511
- `❌ **github_search_code requires authentication**`,
512
- ``,
513
- `GitHub's code-search API does not work for anonymous callers - they return 422.`,
514
- `Other \`github_*\` tools work without a token (rate-limited to 60 req/hr).`,
515
- ``,
516
- `**Setup (one-time):**`,
517
- ``,
518
- `1. Create a personal access token: https://github.com/settings/tokens`,
519
- `2. Minimum scopes needed: \`public_repo\` (or \`repo\` for private)`,
520
- `3. Set the env var in your MCP client config:`,
521
- ``,
522
- ` \`\`\`json`,
523
- ` {`,
524
- ` "mcpServers": {`,
525
- ` "finch": {`,
526
- ` "command": "npx",`,
527
- ` "args": ["-y", "@finchagentic/mcp"],`,
528
- ` "env": { "GITHUB_TOKEN": "ghp_..." }`,
529
- ` }`,
530
- ` }`,
531
- ` }`,
532
- ` \`\`\``,
533
- ``,
534
- `4. Restart the MCP client.`,
535
- ``,
536
- `Authenticated rate limit: 5,000 req/hr (vs 60 unauthenticated for non-search endpoints).`,
537
- ].join("\n"),
538
- }],
539
- isError: true,
540
- };
541
- }
542
- const { query } = a;
543
- const per_page = Math.min(a.per_page ?? 10, 30);
544
- try {
545
- const data = await gh(`/search/code?q=${encodeURIComponent(query)}&per_page=${per_page}`);
546
- const items = data.items ?? [];
547
- const header = `## Code Search — \`${query}\`\n${data.total_count?.toLocaleString()} total results (showing ${items.length})\n`;
548
- const rows = items.map((item) => `**[\`${item.path}\`](${item.html_url})** · ${item.repository?.full_name}`);
549
- return md([header, ...rows].join("\n"), buildCodeSearch(query, data.total_count, items));
550
- }
551
- catch (e) {
552
- return {
553
- content: [{
554
- type: "text",
555
- text: `github_search_code failed: ${e.message}\n\nIf this says "Unprocessable Entity" or 422, your GITHUB_TOKEN may lack required scopes - needs at least \`public_repo\`.`,
556
- }],
557
- isError: true,
558
- };
559
- }
560
- }
561
- default:
562
- return null;
563
- }
564
- }