Skip to content

Commit 28f652e

Browse files
hyperpolymathclaude
andcommitted
feat(mcp-bridge): real GitHub + GitLab API passthrough
Replace stub invocations with actual API calls using fetch(): GitHub (14 tools via boj_github_*): list_repos, get_repo, create_issue, list_issues, get_issue, comment_issue, create_pr, list_prs, get_pr, merge_pr, search_code, search_issues, get_file, graphql GitLab (8 tools via boj_gitlab_*): list_projects, get_project, create_issue, list_issues, create_mr, list_mrs, list_pipelines, setup_mirror Auth via GITHUB_TOKEN and GITLAB_TOKEN environment variables (temporary until vault-mcp zero-knowledge proxy is production). Rate limit headers parsed and returned with responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6ce5762 commit 28f652e

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

mcp-bridge/main.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,153 @@ async function fetchCartridgeInfo(name) {
144144
}
145145
}
146146

147+
// --- Real API passthrough for high-value cartridges ---
148+
// These bypass the BoJ REST API and call services directly when the
149+
// V-lang adapter isn't running. Tokens from environment (temporary)
150+
// or vault-mcp zero-knowledge proxy (production).
151+
152+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || "";
153+
const GITLAB_TOKEN = process.env.GITLAB_TOKEN || "";
154+
155+
async function githubApiCall(method, path, body) {
156+
if (!GITHUB_TOKEN) {
157+
return { error: "GITHUB_TOKEN not set. Store in vault-mcp or export to environment." };
158+
}
159+
try {
160+
const url = `https://api.github.com${path}`;
161+
const opts = {
162+
method,
163+
headers: {
164+
"Authorization": `Bearer ${GITHUB_TOKEN}`,
165+
"Accept": "application/vnd.github+json",
166+
"X-GitHub-Api-Version": "2022-11-28",
167+
"User-Agent": "boj-server/0.3.0",
168+
},
169+
};
170+
if (body && method !== "GET") {
171+
opts.headers["Content-Type"] = "application/json";
172+
opts.body = JSON.stringify(body);
173+
}
174+
const res = await fetch(url, opts);
175+
const data = await res.json();
176+
const rateLimit = {
177+
remaining: res.headers.get("x-ratelimit-remaining"),
178+
reset: res.headers.get("x-ratelimit-reset"),
179+
limit: res.headers.get("x-ratelimit-limit"),
180+
};
181+
return { status: res.status, data, rateLimit };
182+
} catch (err) {
183+
return { error: `GitHub API error: ${err.message}` };
184+
}
185+
}
186+
187+
async function githubGraphQL(query, variables) {
188+
if (!GITHUB_TOKEN) {
189+
return { error: "GITHUB_TOKEN not set." };
190+
}
191+
try {
192+
const res = await fetch("https://api.github.com/graphql", {
193+
method: "POST",
194+
headers: {
195+
"Authorization": `Bearer ${GITHUB_TOKEN}`,
196+
"Content-Type": "application/json",
197+
"User-Agent": "boj-server/0.3.0",
198+
},
199+
body: JSON.stringify({ query, variables: variables || {} }),
200+
});
201+
return await res.json();
202+
} catch (err) {
203+
return { error: `GitHub GraphQL error: ${err.message}` };
204+
}
205+
}
206+
207+
async function gitlabApiCall(method, path, body) {
208+
if (!GITLAB_TOKEN) {
209+
return { error: "GITLAB_TOKEN not set." };
210+
}
211+
const baseUrl = process.env.GITLAB_URL || "https://gitlab.com";
212+
try {
213+
const url = `${baseUrl}/api/v4${path}`;
214+
const opts = {
215+
method,
216+
headers: {
217+
"PRIVATE-TOKEN": GITLAB_TOKEN,
218+
"Accept": "application/json",
219+
"User-Agent": "boj-server/0.3.0",
220+
},
221+
};
222+
if (body && method !== "GET") {
223+
opts.headers["Content-Type"] = "application/json";
224+
opts.body = JSON.stringify(body);
225+
}
226+
const res = await fetch(url, opts);
227+
const data = await res.json();
228+
return { status: res.status, data };
229+
} catch (err) {
230+
return { error: `GitLab API error: ${err.message}` };
231+
}
232+
}
233+
234+
// Route GitHub API tool calls to real API
235+
async function handleGitHubTool(toolName, args) {
236+
switch (toolName) {
237+
case "boj_github_list_repos":
238+
return githubApiCall("GET", `/user/repos?per_page=${args.per_page || 30}&sort=${args.sort || "updated"}`);
239+
case "boj_github_get_repo":
240+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}`);
241+
case "boj_github_create_issue":
242+
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/issues`, { title: args.title, body: args.body, labels: args.labels });
243+
case "boj_github_list_issues":
244+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/issues?state=${args.state || "open"}&per_page=${args.per_page || 30}`);
245+
case "boj_github_get_issue":
246+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/issues/${args.issue_number}`);
247+
case "boj_github_comment_issue":
248+
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/issues/${args.issue_number}/comments`, { body: args.body });
249+
case "boj_github_create_pr":
250+
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/pulls`, { title: args.title, body: args.body, head: args.head, base: args.base || "main" });
251+
case "boj_github_list_prs":
252+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/pulls?state=${args.state || "open"}`);
253+
case "boj_github_get_pr":
254+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/pulls/${args.pull_number}`);
255+
case "boj_github_merge_pr":
256+
return githubApiCall("PUT", `/repos/${args.owner}/${args.repo}/pulls/${args.pull_number}/merge`, { merge_method: args.method || "merge" });
257+
case "boj_github_search_code":
258+
return githubApiCall("GET", `/search/code?q=${encodeURIComponent(args.query)}`);
259+
case "boj_github_search_issues":
260+
return githubApiCall("GET", `/search/issues?q=${encodeURIComponent(args.query)}`);
261+
case "boj_github_get_file":
262+
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/contents/${args.path}?ref=${args.ref || "main"}`);
263+
case "boj_github_graphql":
264+
return githubGraphQL(args.query, args.variables);
265+
default:
266+
return { error: `Unknown GitHub tool: ${toolName}` };
267+
}
268+
}
269+
270+
// Route GitLab API tool calls to real API
271+
async function handleGitLabTool(toolName, args) {
272+
switch (toolName) {
273+
case "boj_gitlab_list_projects":
274+
return gitlabApiCall("GET", `/projects?owned=true&per_page=${args.per_page || 20}`);
275+
case "boj_gitlab_get_project":
276+
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}`);
277+
case "boj_gitlab_create_issue":
278+
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/issues`, { title: args.title, description: args.description });
279+
case "boj_gitlab_list_issues":
280+
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/issues?state=${args.state || "opened"}`);
281+
case "boj_gitlab_create_mr":
282+
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/merge_requests`, { title: args.title, source_branch: args.source, target_branch: args.target || "main" });
283+
case "boj_gitlab_list_mrs":
284+
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/merge_requests?state=${args.state || "opened"}`);
285+
case "boj_gitlab_list_pipelines":
286+
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/pipelines`);
287+
case "boj_gitlab_setup_mirror":
288+
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/remote_mirrors`, { url: args.target_url, enabled: true });
289+
default:
290+
return { error: `Unknown GitLab tool: ${toolName}` };
291+
}
292+
}
293+
147294
// --- Build MCP tool list from BoJ cartridges ---
148295

149296
function cartridgeToTools(cartridges) {
@@ -378,6 +525,42 @@ function cartridgeToTools(cartridges) {
378525
},
379526
});
380527

528+
// GitHub API (real passthrough)
529+
const ghTools = [
530+
{ name: "boj_github_list_repos", desc: "List your GitHub repositories", props: { per_page: { type: "number" }, sort: { type: "string", enum: ["updated", "created", "pushed", "full_name"] } } },
531+
{ name: "boj_github_get_repo", desc: "Get a GitHub repository", props: { owner: { type: "string" }, repo: { type: "string" } }, req: ["owner", "repo"] },
532+
{ name: "boj_github_create_issue", desc: "Create an issue on a GitHub repo", props: { owner: { type: "string" }, repo: { type: "string" }, title: { type: "string" }, body: { type: "string" }, labels: { type: "array", items: { type: "string" } } }, req: ["owner", "repo", "title"] },
533+
{ name: "boj_github_list_issues", desc: "List issues on a GitHub repo", props: { owner: { type: "string" }, repo: { type: "string" }, state: { type: "string", enum: ["open", "closed", "all"] }, per_page: { type: "number" } }, req: ["owner", "repo"] },
534+
{ name: "boj_github_get_issue", desc: "Get a specific issue", props: { owner: { type: "string" }, repo: { type: "string" }, issue_number: { type: "number" } }, req: ["owner", "repo", "issue_number"] },
535+
{ name: "boj_github_comment_issue", desc: "Comment on an issue", props: { owner: { type: "string" }, repo: { type: "string" }, issue_number: { type: "number" }, body: { type: "string" } }, req: ["owner", "repo", "issue_number", "body"] },
536+
{ name: "boj_github_create_pr", desc: "Create a pull request", props: { owner: { type: "string" }, repo: { type: "string" }, title: { type: "string" }, body: { type: "string" }, head: { type: "string" }, base: { type: "string" } }, req: ["owner", "repo", "title", "head"] },
537+
{ name: "boj_github_list_prs", desc: "List pull requests", props: { owner: { type: "string" }, repo: { type: "string" }, state: { type: "string", enum: ["open", "closed", "all"] } }, req: ["owner", "repo"] },
538+
{ name: "boj_github_get_pr", desc: "Get a specific pull request", props: { owner: { type: "string" }, repo: { type: "string" }, pull_number: { type: "number" } }, req: ["owner", "repo", "pull_number"] },
539+
{ name: "boj_github_merge_pr", desc: "Merge a pull request", props: { owner: { type: "string" }, repo: { type: "string" }, pull_number: { type: "number" }, method: { type: "string", enum: ["merge", "squash", "rebase"] } }, req: ["owner", "repo", "pull_number"] },
540+
{ name: "boj_github_search_code", desc: "Search code on GitHub", props: { query: { type: "string" } }, req: ["query"] },
541+
{ name: "boj_github_search_issues", desc: "Search issues and PRs on GitHub", props: { query: { type: "string" } }, req: ["query"] },
542+
{ name: "boj_github_get_file", desc: "Get file contents from a repo", props: { owner: { type: "string" }, repo: { type: "string" }, path: { type: "string" }, ref: { type: "string" } }, req: ["owner", "repo", "path"] },
543+
{ name: "boj_github_graphql", desc: "Execute a GitHub GraphQL query", props: { query: { type: "string" }, variables: { type: "object" } }, req: ["query"] },
544+
];
545+
for (const t of ghTools) {
546+
tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [] } });
547+
}
548+
549+
// GitLab API (real passthrough)
550+
const glTools = [
551+
{ name: "boj_gitlab_list_projects", desc: "List your GitLab projects", props: { per_page: { type: "number" } } },
552+
{ name: "boj_gitlab_get_project", desc: "Get a GitLab project", props: { project_id: { type: "string", description: "Project ID or URL-encoded path" } }, req: ["project_id"] },
553+
{ name: "boj_gitlab_create_issue", desc: "Create a GitLab issue", props: { project_id: { type: "string" }, title: { type: "string" }, description: { type: "string" } }, req: ["project_id", "title"] },
554+
{ name: "boj_gitlab_list_issues", desc: "List GitLab project issues", props: { project_id: { type: "string" }, state: { type: "string", enum: ["opened", "closed", "all"] } }, req: ["project_id"] },
555+
{ name: "boj_gitlab_create_mr", desc: "Create a merge request", props: { project_id: { type: "string" }, title: { type: "string" }, source: { type: "string" }, target: { type: "string" } }, req: ["project_id", "title", "source"] },
556+
{ name: "boj_gitlab_list_mrs", desc: "List merge requests", props: { project_id: { type: "string" }, state: { type: "string", enum: ["opened", "closed", "merged", "all"] } }, req: ["project_id"] },
557+
{ name: "boj_gitlab_list_pipelines", desc: "List CI/CD pipelines", props: { project_id: { type: "string" } }, req: ["project_id"] },
558+
{ name: "boj_gitlab_setup_mirror", desc: "Set up a push mirror", props: { project_id: { type: "string" }, target_url: { type: "string" } }, req: ["project_id", "target_url"] },
559+
];
560+
for (const t of glTools) {
561+
tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [] } });
562+
}
563+
381564
// Research
382565
tools.push({
383566
name: "boj_research",
@@ -515,6 +698,36 @@ async function handleMessage(line) {
515698
sendResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
516699
break;
517700
}
701+
case "boj_github_list_repos":
702+
case "boj_github_get_repo":
703+
case "boj_github_create_issue":
704+
case "boj_github_list_issues":
705+
case "boj_github_get_issue":
706+
case "boj_github_comment_issue":
707+
case "boj_github_create_pr":
708+
case "boj_github_list_prs":
709+
case "boj_github_get_pr":
710+
case "boj_github_merge_pr":
711+
case "boj_github_search_code":
712+
case "boj_github_search_issues":
713+
case "boj_github_get_file":
714+
case "boj_github_graphql": {
715+
const result = await handleGitHubTool(toolName, args);
716+
sendResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
717+
break;
718+
}
719+
case "boj_gitlab_list_projects":
720+
case "boj_gitlab_get_project":
721+
case "boj_gitlab_create_issue":
722+
case "boj_gitlab_list_issues":
723+
case "boj_gitlab_create_mr":
724+
case "boj_gitlab_list_mrs":
725+
case "boj_gitlab_list_pipelines":
726+
case "boj_gitlab_setup_mirror": {
727+
const result = await handleGitLabTool(toolName, args);
728+
sendResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
729+
break;
730+
}
518731
case "boj_research": {
519732
const result = await invokeCartridge("research-mcp", args);
520733
sendResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });

0 commit comments

Comments
 (0)