Add github_search, github_file, http_fetch tools to chat AI

Gemini can now:
- Search GitHub for MIT-licensed OSS repos matching any description
- Read specific files from any public repo (READMEs, design systems,
  package.json, docker-compose.yml, component libraries, etc.)
- Fetch any public URL for docs, APIs, or reference material

No hardcoded pipelines — Gemini decides how to use these tools
based on what the user asks for.

Made-with: Cursor
This commit is contained in:
2026-04-27 15:58:02 -07:00
parent 1e138d69d6
commit c41d018d79

View File

@@ -5,6 +5,8 @@
*/
import type { ToolDefinition } from './gemini-chat';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [
{
name: 'projects_list',
@@ -85,6 +87,85 @@ export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [
required: ['appUuid'],
},
},
{
name: 'github_search',
description:
'Search GitHub for public repositories. Use this to find open source projects, ' +
'reference implementations, design systems, or starting points. ' +
'Always add "license:mit" to the query to ensure permissive licensing. ' +
'Example queries: "license:mit self-hosted crm", "license:mit kanban board react", ' +
'"license:mit design-system components".',
parameters: {
type: 'OBJECT',
properties: {
query: {
type: 'STRING',
description:
'GitHub search query. Include license:mit unless you have a reason not to. ' +
'You can filter by language (language:typescript), stars (stars:>500), ' +
'topics (topic:self-hosted), and file presence (filename:docker-compose.yml).',
},
sort: {
type: 'STRING',
description: 'Sort by: "stars" (default), "updated", or "forks".',
},
limit: {
type: 'NUMBER',
description: 'Number of results to return (default 8, max 20).',
},
},
required: ['query'],
},
},
{
name: 'github_file',
description:
'Read a specific file from a public GitHub repository. ' +
'Use this to study a project\'s design system, component library, README, ' +
'package.json, docker-compose.yml, or any other file. ' +
'Great for understanding how an existing open source project is structured ' +
'before building something inspired by it.',
parameters: {
type: 'OBJECT',
properties: {
repo: {
type: 'STRING',
description: 'Repository in "owner/repo" format (e.g. "makeplane/plane").',
},
path: {
type: 'STRING',
description: 'File path within the repo (e.g. "README.md", "packages/ui/src/index.ts", "docker-compose.yml").',
},
ref: {
type: 'STRING',
description: 'Branch or commit ref (default: "main").',
},
},
required: ['repo', 'path'],
},
},
{
name: 'http_fetch',
description:
'Fetch any public URL and return the response body as text. ' +
'Use for reading documentation, API responses, public datasets, ' +
'or any web resource relevant to the user\'s request. ' +
'Response is truncated to 12KB to fit context.',
parameters: {
type: 'OBJECT',
properties: {
url: {
type: 'STRING',
description: 'The full URL to fetch (must be publicly accessible, https preferred).',
},
headers: {
type: 'OBJECT',
description: 'Optional HTTP headers as key-value pairs.',
},
},
required: ['url'],
},
},
];
/**
@@ -97,6 +178,11 @@ export async function executeMcpTool(
mcpToken: string,
baseUrl: string,
): Promise<string> {
// Handle non-MCP tools directly
if (toolName === 'github_search') return executeGithubSearch(args);
if (toolName === 'github_file') return executeGithubFile(args);
if (toolName === 'http_fetch') return executeHttpFetch(args);
// Map tool names (underscored) back to MCP action names (dotted)
const actionMap: Record<string, string> = {
projects_list: 'projects.list',
@@ -130,3 +216,120 @@ export async function executeMcpTool(
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
}
}
// ── Non-MCP tool implementations ──────────────────────────────────────────────
async function executeGithubSearch(args: Record<string, unknown>): Promise<string> {
const query = String(args.query || '');
const sort = String(args.sort || 'stars');
const limit = Math.min(Number(args.limit || 8), 20);
try {
const params = new URLSearchParams({
q: query,
sort,
order: 'desc',
per_page: String(limit),
});
const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
};
if (GITHUB_TOKEN) headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
const res = await fetch(`https://api.github.com/search/repositories?${params}`, { headers });
const data = await res.json();
if (!res.ok) return JSON.stringify({ error: data.message || 'GitHub API error' });
const repos = (data.items || []).map((r: any) => ({
name: r.full_name,
description: r.description,
stars: r.stargazers_count,
language: r.language,
license: r.license?.spdx_id,
topics: r.topics,
hasDockerfile: r.topics?.includes('docker') || false,
updatedAt: r.pushed_at?.slice(0, 10),
url: r.html_url,
defaultBranch: r.default_branch,
}));
return JSON.stringify({ total: data.total_count, repos }, null, 2);
} catch (e) {
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
}
}
async function executeGithubFile(args: Record<string, unknown>): Promise<string> {
const repo = String(args.repo || '');
const path = String(args.path || '');
const ref = String(args.ref || 'main');
try {
const headers: Record<string, string> = {
Accept: 'application/vnd.github.raw+json',
'X-GitHub-Api-Version': '2022-11-28',
};
if (GITHUB_TOKEN) headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
const res = await fetch(
`https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`,
{ headers },
);
if (!res.ok) {
// Try alternate branch if main fails
if (ref === 'main') {
const res2 = await fetch(
`https://api.github.com/repos/${repo}/contents/${path}?ref=master`,
{ headers },
);
if (res2.ok) {
const text = await res2.text();
return text.slice(0, 12000);
}
}
return JSON.stringify({ error: `File not found: ${repo}/${path} (ref: ${ref})` });
}
const text = await res.text();
// GitHub returns base64-encoded content for files; raw+json accept header gets raw text
return text.slice(0, 12000);
} catch (e) {
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
}
}
async function executeHttpFetch(args: Record<string, unknown>): Promise<string> {
const url = String(args.url || '');
const extraHeaders = (args.headers as Record<string, string>) || {};
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return JSON.stringify({ error: 'URL must start with http:// or https://' });
}
try {
const res = await fetch(url, {
headers: {
'User-Agent': 'Vibn-AI/1.0',
...extraHeaders,
},
signal: AbortSignal.timeout(10_000),
});
const contentType = res.headers.get('content-type') || '';
let body: string;
if (contentType.includes('json')) {
const json = await res.json();
body = JSON.stringify(json, null, 2);
} else {
body = await res.text();
}
return `HTTP ${res.status}\nContent-Type: ${contentType}\n\n${body}`.slice(0, 12000);
} catch (e) {
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
}
}