@@ -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
149296function 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