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:
@@ -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) });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user