bluelamp 6.1.0 → 6.2.0
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.
- package/bin/bluelamp-vscode-base +1 -1
- package/lib/auth/InteractiveLogin.js +1 -1
- package/lib/auth/PortalAuthClient.js +1 -1
- package/lib/auth/TokenManager.js +1 -1
- package/lib/mcp-server/index.cjs +1 -1
- package/lib/mcp-server/portal-api-client.cjs +1 -1
- package/package.json +1 -1
- package/scripts/setup-mcps.js +1 -1
package/lib/mcp-server/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const{Server:Server}=require("@modelcontextprotocol/sdk/server/index.js"),{StdioServerTransport:StdioServerTransport}=require("@modelcontextprotocol/sdk/server/stdio.js"),{CallToolRequestSchema:CallToolRequestSchema,ListToolsRequestSchema:ListToolsRequestSchema}=require("@modelcontextprotocol/sdk/types.js"),{getPromptFromPortal:getPromptFromPortal,listAvailableMcps:listAvailableMcps,downloadMcpPackage:downloadMcpPackage}=require("./portal-api-client.cjs"),{logger:logger}=require("../utils/logger"),{t:t,setLanguage:setLanguage}=require("../i18n"),{spawn:spawn}=require("child_process"),path=require("path"),fs=require("fs"),os=require("os"),dotenvLocalPath=path.join(__dirname,"../../.env.local"),dotenvPath=path.join(__dirname,"../../.env");fs.existsSync(dotenvLocalPath)?(require("dotenv").config({path:dotenvLocalPath}),logger.debug("[MCP Server] Loaded .env.local")):fs.existsSync(dotenvPath)&&(require("dotenv").config({path:dotenvPath}),logger.debug("[MCP Server] Loaded .env"));const KEYWORD=process.env.MUSUHI_KEYWORD||"BL-要件定義",TOKEN=process.env.MUSUHI_TOKEN,isDevEnv="development"===process.env.NODE_ENV||process.env.PORTAL_URL&&process.env.PORTAL_URL.includes("localhost"),PORTAL_URL=process.env.PORTAL_URL||(isDevEnv?"http://localhost:8432":"https://motohi-api-235426778039.asia-northeast1.run.app"),LANGUAGE=process.env.MUSUHI_LANGUAGE||"ja";setLanguage(LANGUAGE);const LOG_DIR=path.join(os.homedir(),".bluelamp-vscode","logs"),LOG_FILE=path.join(LOG_DIR,"mcp.log");fs.existsSync(LOG_DIR)||fs.mkdirSync(LOG_DIR,{recursive:!0});const REGISTRY_DIR=path.join(os.homedir(),".bluelamp"),REGISTRY_FILE=path.join(REGISTRY_DIR,"registry.json");function log(e){const t=`[${(new Date).toISOString()}] ${e}\n`;fs.appendFileSync(LOG_FILE,t),logger.info(`[MCP Server] ${e}`)}function loadRegistry(){if(!fs.existsSync(REGISTRY_FILE))return{};try{return JSON.parse(fs.readFileSync(REGISTRY_FILE,"utf-8"))}catch(e){return log(`[Registry] Parse error: ${e.message}`),{}}}function saveRegistry(e){fs.existsSync(REGISTRY_DIR)||fs.mkdirSync(REGISTRY_DIR,{recursive:!0}),fs.writeFileSync(REGISTRY_FILE,JSON.stringify(e,null,2)+"\n")}function searchServices(e,t){const r=e.toLowerCase().split(/\s+/),s=[];for(const[e,o]of Object.entries(t)){let t=0;const n=[e,o.name||"",o.description||"",...o.tags||[]].join(" ").toLowerCase();for(const e of r)n.includes(e)&&t++;t>0&&s.push({id:e,service:o,score:t})}return s.sort((e,t)=>t.score-e.score),s}log(`MCP Server started - Log file: ${LOG_FILE}`),log("Environment variables:"),log(` KEYWORD: ${KEYWORD}`),log(" TOKEN: "+(TOKEN?TOKEN.substring(0,20)+"...":"NOT SET")),log(` PORTAL_URL: ${PORTAL_URL}`),log(` LANGUAGE: ${LANGUAGE}`);class ChildMcpProcess{constructor(e,t,r,s){this.serviceId=e,this.command=t,this.args=r,this.env={...process.env,...s},this.process=null,this.tools=[],this.requestId=0,this.pending=new Map,this.buffer=""}async start(){return new Promise((e,t)=>{const r=setTimeout(()=>{t(new Error(`Timeout: "${this.serviceId}" failed to start within 30s`)),this.stop()},3e4);try{this.process=spawn(this.command,this.args,{env:this.env,stdio:["pipe","pipe","pipe"]}),this.process.on("error",e=>{clearTimeout(r),t(new Error(`Spawn error for "${this.serviceId}": ${e.message}`))}),this.process.on("exit",(e,t)=>{log(`[ChildMcp] "${this.serviceId}" exited (code=${e}, signal=${t})`),this.pending.forEach(e=>e.reject(new Error("Process exited"))),this.pending.clear()}),this.process.stderr.on("data",e=>{log(`[ChildMcp:${this.serviceId}:stderr] ${e.toString().trim()}`)}),this.process.stdout.on("data",e=>{this.buffer+=e.toString(),this._processBuffer()}),this._sendRequest("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"bluelamp-gateway",version:"1.0.0"}}).then(e=>(log(`[ChildMcp] "${this.serviceId}" initialized: ${JSON.stringify(e.serverInfo||{})}`),this._sendNotification("notifications/initialized",{}),this._sendRequest("tools/list",{}))).then(t=>{const s=t.tools||[];this.tools=s.map(e=>({...e,name:`${this.serviceId}__${e.name}`,_originalName:e.name})),log(`[ChildMcp] "${this.serviceId}" ready: ${this.tools.length} tools`),clearTimeout(r),e(this.tools)}).catch(e=>{clearTimeout(r),t(e)})}catch(e){clearTimeout(r),t(e)}})}async callTool(e,t){return await this._sendRequest("tools/call",{name:e,arguments:t})}stop(){this.process&&(this.process.kill(),this.process=null),this.pending.forEach(e=>e.reject(new Error("Process stopped"))),this.pending.clear()}_sendRequest(e,t){return new Promise((r,s)=>{const o=++this.requestId,n=JSON.stringify({jsonrpc:"2.0",id:o,method:e,params:t})+"\n";this.pending.set(o,{resolve:r,reject:s});try{this.process.stdin.write(n)}catch(e){this.pending.delete(o),s(new Error(`Write error: ${e.message}`))}})}_sendNotification(e,t){const r=JSON.stringify({jsonrpc:"2.0",method:e,params:t})+"\n";try{this.process.stdin.write(r)}catch(e){log(`[ChildMcp] Notification write error: ${e.message}`)}}_processBuffer(){const e=this.buffer.split("\n");this.buffer=e.pop();for(const t of e){const e=t.trim();if(e)try{const t=JSON.parse(e);if(t.id&&this.pending.has(t.id)){const e=this.pending.get(t.id);this.pending.delete(t.id),t.error?e.reject(new Error(`JSON-RPC error: ${JSON.stringify(t.error)}`)):e.resolve(t.result)}}catch(t){log(`[ChildMcp:${this.serviceId}] Parse error: ${t.message} | line: ${e.substring(0,200)}`)}}}}class ChildMcpManager{constructor(){this.children=new Map}async activateExternal(e,t){const r=new ChildMcpProcess(e,t.command,t.args||[],t.env||{}),s=await r.start();return this.children.set(e,{type:"external",process:r,tools:s}),s}deactivate(e){const t=this.children.get(e);t&&("external"===t.type&&t.process&&t.process.stop(),this.children.delete(e))}isActive(e){return this.children.has(e)}getActiveTools(){const e=[];for(const t of this.children.values())e.push(...t.tools||[]);return e}async callTool(e,t){for(const[r,s]of this.children)if("external"===s.type){const r=s.tools.find(t=>t.name===e);if(r)return{found:!0,result:await s.process.callTool(r._originalName,t)}}return{found:!1}}getActiveServiceIds(){return[...this.children.keys()]}}class BlueLampServer{constructor(){this.childManager=new ChildMcpManager,this.server=new Server({name:"bluelamp",version:"2.0.0"},{capabilities:{tools:{listChanged:!0}}}),this.setupHandlers()}checkAuth(){return TOKEN?{ok:!0}:{ok:!1,error:"MUSUHI_TOKEN が設定されていません。BlueLampにログインしてください。"}}buildToolList(){const e=[{name:"load_knowledge",description:t("mcp.toolDescriptionLoadKnowledge"),annotations:{readOnlyHint:!0,idempotentHint:!0},inputSchema:{type:"object",properties:{keyword:{type:"string",description:t("mcp.keywordDescription")}},required:["keyword"]}},{name:"search_mcp",description:'キーワードでインストール済みMCPサービスを検索します。使いたい機能を自然言語で説明してください。(例: "ブラウザ操作", "メール送信", "画像生成")',inputSchema:{type:"object",properties:{query:{type:"string",description:"検索キーワード(自然言語OK)"}},required:["query"]}},{name:"activate_service",description:"MCPサービスを有効化してツールを追加します。search_mcpで見つけたservice_idを指定してください。always_on: trueで次回以降も自動的に有効化されます。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"},always_on:{type:"boolean",description:"trueにすると常時アクティブ(毎回自動起動)になります"}},required:["service_id"]}},{name:"deactivate_service",description:"MCPサービスの常時アクティブ(alwaysOn)設定を解除します。次回起動時から自動有効化されなくなります。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"}},required:["service_id"]}},{name:"register_service",description:"新しいMCPサービスをゲートウェイに登録します。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:'サービスID(英数字、例: "microcms")'},name:{type:"string",description:"サービス表示名"},description:{type:"string",description:"サービスの説明"},tags:{type:"array",items:{type:"string"},description:"検索用タグ"},command:{type:"string",description:'実行コマンド("npx" or "node")'},args:{type:"array",items:{type:"string"},description:"コマンド引数"},env:{type:"object",description:"環境変数"}},required:["service_id","name","description","tags"]}},{name:"browse_mcp_store",description:"MCPストア(Portal)からインストール可能なMCP一覧を取得します。",inputSchema:{type:"object",properties:{category:{type:"string",description:"カテゴリでフィルタ(省略可)"}},required:[]}},{name:"install_from_store",description:"MCPストアからMCPをダウンロードしてインストールします。browse_mcp_storeで見つけたmcp_idを指定してください。",inputSchema:{type:"object",properties:{mcp_id:{type:"string",description:"インストールするMCPのID"}},required:["mcp_id"]}}];return e.push(...this.childManager.getActiveTools()),e}setupHandlers(){this.server.setRequestHandler(ListToolsRequestSchema,async()=>{const e=this.buildToolList(),t=this.childManager.getActiveServiceIds();return log(`[ListTools] ${e.length} tools (active: ${t.join(", ")||"none"})`),{tools:e}}),this.server.setRequestHandler(CallToolRequestSchema,async e=>{const{name:r,arguments:s}=e.params;if(log(`[CallTool] ${r}`),"load_knowledge"===r){const{keyword:e}=s;log(`[load_knowledge] keyword: ${e}`);try{const r=await getPromptFromPortal(e,TOKEN,LANGUAGE);if(!r||!r.success){const s=r?.error||t("mcp.knowledgeNotFound",{keyword:e});return{content:[{type:"text",text:`❌ ${t("mcp.portalApiFailed",{error:s})}`}],isError:!0}}const s="ja"!==LANGUAGE?"\n\n[Note] The user's preferred language is English.":"";return{content:[{type:"text",text:`✅ ${t("mcp.knowledgeLoadSuccess")}\n\n${r.knowledge}${s}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${t("mcp.knowledgeApplied")}`}]}}catch(e){return log(`[load_knowledge] Exception: ${e.message}`),{content:[{type:"text",text:`❌ ${t("mcp.knowledgeLoadError",{error:e.message})}`}],isError:!0}}}if("search_mcp"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const t=loadRegistry(),r=searchServices(s.query,t);if(0===r.length)return{content:[{type:"text",text:`🔍 「${s.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、register_serviceで新しいサービスを登録してください。`}]};let o=`🔍 「${s.query}」の検索結果(${r.length}件)\n\n`;for(const{id:e,service:t}of r.slice(0,5)){const r=this.childManager.isActive(e),s=t.alwaysOn,n=s?" [常時アクティブ]":"";o+=`${s?"🔒":r?"🟢":"⚪"} **${t.name}**${n} (id: \`${e}\`)\n`,o+=` ${t.description}\n`,t.tags&&(o+=` タグ: ${t.tags.join(", ")}\n`),o+="\n"}return o+='有効化するには: activate_service(service_id: "...", always_on: true で常時アクティブ)',{content:[{type:"text",text:o}]}}if("activate_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,always_on:r}=s,o=loadRegistry(),n=o[t];if(!n)return{content:[{type:"text",text:`❌ サービス "${t}" は登録されていません。search_mcpで検索してください。`}],isError:!0};if(r&&(o[t].alwaysOn=!0,saveRegistry(o),log(`[activate] "${t}" set alwaysOn: true`)),this.childManager.isActive(t))return{content:[{type:"text",text:`ℹ️ "${t}" は既に有効です。${r?"(常時アクティブに設定しました)":""}`}]};let i;try{i=await this.childManager.activateExternal(t,n)}catch(e){return log(`[activate] Failed "${t}": ${e.message}`),{content:[{type:"text",text:`❌ "${t}" の起動に失敗しました: ${e.message}`}],isError:!0}}const a=i.map(e=>e.name).join(", "),c=r?" 🔒常時アクティブ":"";log(`[activate] "${t}" → ${i.length} tools${r?" (alwaysOn)":""}. Sending list_changed.`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[activate] notification error: ${e.message}`)}return{content:[{type:"text",text:`✅ "${n.name}" を有効化しました${c}(ツール${i.length}個追加)\n\n追加ツール: ${a||"(なし)"}`}]}}if("deactivate_service"===r){const{service_id:e}=s,t=loadRegistry(),r=t[e];return r?r.alwaysOn?(t[e].alwaysOn=!1,saveRegistry(t),log(`[deactivate] "${e}" set alwaysOn: false`),{content:[{type:"text",text:`✅ "${e}" の常時アクティブを解除しました。次回起動時から自動有効化されません。`}]}):{content:[{type:"text",text:`ℹ️ "${e}" は常時アクティブではありません。変更の必要はありません。`}]}:{content:[{type:"text",text:`❌ サービス "${e}" は登録されていません。`}],isError:!0}}if("register_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,name:r,description:o,tags:n,command:i,args:a,env:c}=s,l=loadRegistry(),p=!!l[t];return l[t]={name:r,description:o,tags:n||[],type:"external",command:i||null,args:a||[],env:c||{}},saveRegistry(l),log(`[register] "${t}" ${p?"updated":"created"}`),{content:[{type:"text",text:`✅ サービス "${r}" を${p?"更新":"登録"}しました (id: ${t})\n\nタグ: ${(n||[]).join(", ")}\n\nsearch_mcp で検索可能になりました。`}]}}if("browse_mcp_store"===r){const{category:e}=s||{};log(`[browse_mcp_store] category=${e||"all"}`);try{const t=e?{category:e}:{},r=await listAvailableMcps(t,TOKEN,LANGUAGE);if(!r||!r.success)return{content:[{type:"text",text:`❌ MCPカタログの取得に失敗: ${r?.error||"Unknown"}`}],isError:!0};const s=r.data||[];if(0===s.length)return{content:[{type:"text",text:"📦 インストール可能なMCPはありません。"}]};let o=`📦 MCPストア一覧 (${s.length}件)\n\n`;return s.forEach(e=>{const t=e.hasAccess?"✅":"🔒",r=e.isOfficial?" [公式]":"";o+=`${t} **${e.name}**${r}\n`,o+=` ID: ${e.mcpId}\n`,o+=` ${e.description}\n`,o+=` カテゴリ: ${e.category} | ツール数: ${e.toolCount}\n\n`}),o+='\nインストールするには: install_from_store(mcp_id: "MCP_ID")',{content:[{type:"text",text:o}]}}catch(e){return log(`[browse_mcp_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}if("install_from_store"===r){const{mcp_id:e}=s;log(`[install_from_store] mcp_id=${e}`);try{const t=await downloadMcpPackage(e,TOKEN);if(!t||!t.success)return{content:[{type:"text",text:`❌ MCPダウンロードに失敗: ${t?.error||"Unknown"}`}],isError:!0};const r=t.data,s=r.files||[];if(0===s.length)return{content:[{type:"text",text:"❌ MCPパッケージにファイルが含まれていません。"}],isError:!0};const o=path.join(os.homedir(),".claude","mcp-servers",e);fs.mkdirSync(o,{recursive:!0});for(const e of s){const t=path.join(o,e.path);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(t,e.content,"utf-8")}log(`[install_from_store] ${s.length} files written to ${o}`);const n=r.packageSource||{};if(n.needsNpmInstall&&fs.existsSync(path.join(o,"package.json"))){const{execSync:e}=require("child_process");try{e("npm install --production",{cwd:o,timeout:6e4,stdio:"pipe"})}catch(e){return{content:[{type:"text",text:`⚠️ ファイル展開完了、npm install失敗: ${e.message}`}]}}}const i=n.entryPoint||"lib/index.cjs",a=loadRegistry();a[e]={name:n.name||e,description:n.description||"",tags:n.tags||[],type:"external",command:"node",args:[path.join(o,i)],env:{}};const c=n.requiredEnvVars||[];for(const t of c)a[e].env[t]=process.env[t]||`<SET_YOUR_${t}>`;saveRegistry(a);let l=`✅ MCP「${n.name||e}」をインストールしました\n\n`;l+=`📁 インストール先: ${o}\n`,l+=`📄 ファイル数: ${s.length}\n`,l+="⚙️ registry.json に登録済み\n",n.installNotes&&(l+=`\n📝 ${n.installNotes}\n`);const p=c.filter(e=>!process.env[e]);return p.length>0&&(l+="\n⚠️ 以下の環境変数をregistry.jsonで設定してください:\n",p.forEach(e=>{l+=` - ${e}\n`})),l+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:l}]}}catch(e){return log(`[install_from_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}try{const{found:e,result:t}=await this.childManager.callTool(r,s);if(e)return t&&t.content?t:{content:[{type:"text",text:String(t)}]}}catch(e){return log(`[callTool] Error proxying "${r}": ${e.message}`),{content:[{type:"text",text:`❌ ツール実行エラー: ${e.message}`}],isError:!0}}return{content:[{type:"text",text:`❌ Unknown tool: ${r}`}],isError:!0}})}async autoActivate(){const e=loadRegistry();for(const[t,r]of Object.entries(e))if(r.autoActivate||r.alwaysOn){log(`[autoActivate] Activating "${t}" (${r.alwaysOn?"alwaysOn":"autoActivate"})...`);try{log(`[autoActivate] "${t}" ready: ${(await this.childManager.activateExternal(t,r)).length} tools`)}catch(e){log(`[autoActivate] "${t}" failed: ${e.message}`)}}}async run(){const e=new StdioServerTransport;if(await this.server.connect(e),log("[Gateway] BlueLamp Gateway v2.0.0 started."),await this.autoActivate(),this.childManager.getActiveServiceIds().length>0)try{await this.server.notification({method:"notifications/tools/list_changed"}),log("[Gateway] Initial list_changed sent after autoActivate.")}catch(e){log(`[Gateway] Initial list_changed error: ${e.message}`)}if(KEYWORD&&TOKEN){log(`[Gateway] Auto-loading knowledge for keyword: ${KEYWORD}`);const e=await getPromptFromPortal(KEYWORD,TOKEN,LANGUAGE);e&&e.success?log(`[Gateway] Auto-load successful: ${e.title}`):log(`[Gateway] Auto-load failed for keyword: ${KEYWORD}`)}}}const server=new BlueLampServer;server.run().catch(e=>{log(`[Gateway] Fatal: ${e.message}`),process.exit(1)});
|
|
2
|
+
const{Server:Server}=require("@modelcontextprotocol/sdk/server/index.js"),{StdioServerTransport:StdioServerTransport}=require("@modelcontextprotocol/sdk/server/stdio.js"),{CallToolRequestSchema:CallToolRequestSchema,ListToolsRequestSchema:ListToolsRequestSchema}=require("@modelcontextprotocol/sdk/types.js"),{getPromptFromPortal:getPromptFromPortal,listAvailableMcps:listAvailableMcps,downloadMcpPackage:downloadMcpPackage}=require("./portal-api-client.cjs"),{logger:logger}=require("../utils/logger"),{t:t,setLanguage:setLanguage}=require("../i18n"),{spawn:spawn}=require("child_process"),path=require("path"),fs=require("fs"),os=require("os"),dotenvLocalPath=path.join(__dirname,"../../.env.local"),dotenvPath=path.join(__dirname,"../../.env");fs.existsSync(dotenvLocalPath)?(require("dotenv").config({path:dotenvLocalPath}),logger.debug("[MCP Server] Loaded .env.local")):fs.existsSync(dotenvPath)&&(require("dotenv").config({path:dotenvPath}),logger.debug("[MCP Server] Loaded .env"));const KEYWORD=process.env.MUSUHI_KEYWORD||"BL-要件定義",TOKEN=process.env.MUSUHI_TOKEN,isDevEnv="development"===process.env.NODE_ENV||process.env.PORTAL_URL&&process.env.PORTAL_URL.includes("localhost"),PORTAL_URL=process.env.PORTAL_URL||(isDevEnv?"http://localhost:8432":"https://motohi-api-235426778039.asia-northeast1.run.app"),LANGUAGE=process.env.MUSUHI_LANGUAGE||"ja";setLanguage(LANGUAGE);const LOG_DIR=path.join(os.homedir(),".bluelamp-vscode","logs"),LOG_FILE=path.join(LOG_DIR,"mcp.log");fs.existsSync(LOG_DIR)||fs.mkdirSync(LOG_DIR,{recursive:!0});const REGISTRY_DIR=path.join(os.homedir(),".bluelamp"),REGISTRY_FILE=path.join(REGISTRY_DIR,"registry.json");function log(e){const t=`[${(new Date).toISOString()}] ${e}\n`;fs.appendFileSync(LOG_FILE,t),logger.info(`[MCP Server] ${e}`)}function loadRegistry(){if(!fs.existsSync(REGISTRY_FILE))return{};try{return JSON.parse(fs.readFileSync(REGISTRY_FILE,"utf-8"))}catch(e){return log(`[Registry] Parse error: ${e.message}`),{}}}function saveRegistry(e){fs.existsSync(REGISTRY_DIR)||fs.mkdirSync(REGISTRY_DIR,{recursive:!0}),fs.writeFileSync(REGISTRY_FILE,JSON.stringify(e,null,2)+"\n")}function searchServices(e,t){const r=e.toLowerCase().split(/\s+/),s=[];for(const[e,o]of Object.entries(t)){let t=0;const n=[e,o.name||"",o.description||"",...o.tags||[]].join(" ").toLowerCase();for(const e of r)n.includes(e)&&t++;t>0&&s.push({id:e,service:o,score:t})}return s.sort((e,t)=>t.score-e.score),s}log(`MCP Server started - Log file: ${LOG_FILE}`),log("Environment variables:"),log(` KEYWORD: ${KEYWORD}`),log(" TOKEN: "+(TOKEN?TOKEN.substring(0,20)+"...":"NOT SET")),log(` PORTAL_URL: ${PORTAL_URL}`),log(` LANGUAGE: ${LANGUAGE}`);class ChildMcpProcess{constructor(e,t,r,s){this.serviceId=e,this.command=t,this.args=(r||[]).map(e=>"string"==typeof e?e.replace(/^~\//,os.homedir()+"/"):e),this.env={...process.env,...s},this.process=null,this.tools=[],this.requestId=0,this.pending=new Map,this.buffer=""}async start(){return new Promise((e,t)=>{const r=setTimeout(()=>{t(new Error(`Timeout: "${this.serviceId}" failed to start within 30s`)),this.stop()},3e4);try{this.process=spawn(this.command,this.args,{env:this.env,stdio:["pipe","pipe","pipe"]}),this.process.on("error",e=>{clearTimeout(r),t(new Error(`Spawn error for "${this.serviceId}": ${e.message}`))}),this.process.on("exit",(e,t)=>{log(`[ChildMcp] "${this.serviceId}" exited (code=${e}, signal=${t})`),this.pending.forEach(e=>e.reject(new Error("Process exited"))),this.pending.clear()}),this.process.stderr.on("data",e=>{log(`[ChildMcp:${this.serviceId}:stderr] ${e.toString().trim()}`)}),this.process.stdout.on("data",e=>{this.buffer+=e.toString(),this._processBuffer()}),this._sendRequest("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"bluelamp-gateway",version:"1.0.0"}}).then(e=>(log(`[ChildMcp] "${this.serviceId}" initialized: ${JSON.stringify(e.serverInfo||{})}`),this._sendNotification("notifications/initialized",{}),this._sendRequest("tools/list",{}))).then(t=>{const s=t.tools||[];this.tools=s.map(e=>({...e,name:`${this.serviceId}__${e.name}`,_originalName:e.name})),log(`[ChildMcp] "${this.serviceId}" ready: ${this.tools.length} tools`),clearTimeout(r),e(this.tools)}).catch(e=>{clearTimeout(r),t(e)})}catch(e){clearTimeout(r),t(e)}})}async callTool(e,t){return await this._sendRequest("tools/call",{name:e,arguments:t})}stop(){this.process&&(this.process.kill(),this.process=null),this.pending.forEach(e=>e.reject(new Error("Process stopped"))),this.pending.clear()}_sendRequest(e,t){return new Promise((r,s)=>{const o=++this.requestId,n=JSON.stringify({jsonrpc:"2.0",id:o,method:e,params:t})+"\n";this.pending.set(o,{resolve:r,reject:s});try{this.process.stdin.write(n)}catch(e){this.pending.delete(o),s(new Error(`Write error: ${e.message}`))}})}_sendNotification(e,t){const r=JSON.stringify({jsonrpc:"2.0",method:e,params:t})+"\n";try{this.process.stdin.write(r)}catch(e){log(`[ChildMcp] Notification write error: ${e.message}`)}}_processBuffer(){const e=this.buffer.split("\n");this.buffer=e.pop();for(const t of e){const e=t.trim();if(e)try{const t=JSON.parse(e);if(t.id&&this.pending.has(t.id)){const e=this.pending.get(t.id);this.pending.delete(t.id),t.error?e.reject(new Error(`JSON-RPC error: ${JSON.stringify(t.error)}`)):e.resolve(t.result)}}catch(t){log(`[ChildMcp:${this.serviceId}] Parse error: ${t.message} | line: ${e.substring(0,200)}`)}}}}class ChildMcpManager{constructor(){this.children=new Map}async activateExternal(e,t){const r=new ChildMcpProcess(e,t.command,t.args||[],t.env||{}),s=await r.start();return this.children.set(e,{type:"external",process:r,tools:s}),s}deactivate(e){const t=this.children.get(e);t&&("external"===t.type&&t.process&&t.process.stop(),this.children.delete(e))}isActive(e){return this.children.has(e)}getActiveTools(){const e=[];for(const t of this.children.values())e.push(...t.tools||[]);return e}async callTool(e,t){for(const[r,s]of this.children)if("external"===s.type){const r=s.tools.find(t=>t.name===e);if(r)return{found:!0,result:await s.process.callTool(r._originalName,t)}}return{found:!1}}getActiveServiceIds(){return[...this.children.keys()]}}class BlueLampServer{constructor(){this.childManager=new ChildMcpManager,this.server=new Server({name:"bluelamp",version:"2.0.0"},{capabilities:{tools:{listChanged:!0}}}),this.setupHandlers()}checkAuth(){return TOKEN?{ok:!0}:{ok:!1,error:"MUSUHI_TOKEN が設定されていません。BlueLampにログインしてください。"}}buildToolList(){const e=[{name:"load_knowledge",description:t("mcp.toolDescriptionLoadKnowledge"),annotations:{readOnlyHint:!0,idempotentHint:!0},inputSchema:{type:"object",properties:{keyword:{type:"string",description:t("mcp.keywordDescription")}},required:["keyword"]}},{name:"search_mcp",description:'キーワードでインストール済みMCPサービスを検索します。使いたい機能を自然言語で説明してください。(例: "ブラウザ操作", "メール送信", "画像生成")',inputSchema:{type:"object",properties:{query:{type:"string",description:"検索キーワード(自然言語OK)"}},required:["query"]}},{name:"activate_service",description:"MCPサービスを有効化してツールを追加します。search_mcpで見つけたservice_idを指定してください。always_on: trueで次回以降も自動的に有効化されます。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"},always_on:{type:"boolean",description:"trueにすると常時アクティブ(毎回自動起動)になります"}},required:["service_id"]}},{name:"deactivate_service",description:"MCPサービスの常時アクティブ(alwaysOn)設定を解除します。次回起動時から自動有効化されなくなります。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"}},required:["service_id"]}},{name:"register_service",description:"新しいMCPサービスをゲートウェイに登録します。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:'サービスID(英数字、例: "microcms")'},name:{type:"string",description:"サービス表示名"},description:{type:"string",description:"サービスの説明"},tags:{type:"array",items:{type:"string"},description:"検索用タグ"},command:{type:"string",description:'実行コマンド("npx" or "node")'},args:{type:"array",items:{type:"string"},description:"コマンド引数"},env:{type:"object",description:"環境変数"}},required:["service_id","name","description","tags"]}},{name:"browse_mcp_store",description:"MCPストア(Portal)からインストール可能なMCP一覧を取得します。",inputSchema:{type:"object",properties:{category:{type:"string",description:"カテゴリでフィルタ(省略可)"}},required:[]}},{name:"install_from_store",description:"MCPストアからMCPをダウンロードしてインストールします。browse_mcp_storeで見つけたmcp_idを指定してください。",inputSchema:{type:"object",properties:{mcp_id:{type:"string",description:"インストールするMCPのID"}},required:["mcp_id"]}}];return e.push(...this.childManager.getActiveTools()),e}setupHandlers(){this.server.setRequestHandler(ListToolsRequestSchema,async()=>{const e=this.buildToolList(),t=this.childManager.getActiveServiceIds();return log(`[ListTools] ${e.length} tools (active: ${t.join(", ")||"none"})`),{tools:e}}),this.server.setRequestHandler(CallToolRequestSchema,async e=>{const{name:r,arguments:s}=e.params;if(log(`[CallTool] ${r}`),"load_knowledge"===r){const{keyword:e}=s;log(`[load_knowledge] keyword: ${e}`);try{const r=await getPromptFromPortal(e,TOKEN,LANGUAGE);if(!r||!r.success){const s=r?.error||t("mcp.knowledgeNotFound",{keyword:e});return{content:[{type:"text",text:`❌ ${t("mcp.portalApiFailed",{error:s})}`}],isError:!0}}const s="ja"!==LANGUAGE?"\n\n[Note] The user's preferred language is English.":"";return{content:[{type:"text",text:`✅ ${t("mcp.knowledgeLoadSuccess")}\n\n${r.knowledge}${s}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${t("mcp.knowledgeApplied")}`}]}}catch(e){return log(`[load_knowledge] Exception: ${e.message}`),{content:[{type:"text",text:`❌ ${t("mcp.knowledgeLoadError",{error:e.message})}`}],isError:!0}}}if("search_mcp"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const t=loadRegistry(),r=searchServices(s.query,t);if(0===r.length)return{content:[{type:"text",text:`🔍 「${s.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、register_serviceで新しいサービスを登録してください。`}]};let o=`🔍 「${s.query}」の検索結果(${r.length}件)\n\n`;for(const{id:e,service:t}of r.slice(0,5)){const r=this.childManager.isActive(e),s=t.alwaysOn,n=s?" [常時アクティブ]":"";o+=`${s?"🔒":r?"🟢":"⚪"} **${t.name}**${n} (id: \`${e}\`)\n`,o+=` ${t.description}\n`,t.tags&&(o+=` タグ: ${t.tags.join(", ")}\n`),o+="\n"}return o+='有効化するには: activate_service(service_id: "...", always_on: true で常時アクティブ)',{content:[{type:"text",text:o}]}}if("activate_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,always_on:r}=s,o=loadRegistry(),n=o[t];if(!n)return{content:[{type:"text",text:`❌ サービス "${t}" は登録されていません。search_mcpで検索してください。`}],isError:!0};if(r&&(o[t].alwaysOn=!0,saveRegistry(o),log(`[activate] "${t}" set alwaysOn: true`)),this.childManager.isActive(t))return{content:[{type:"text",text:`ℹ️ "${t}" は既に有効です。${r?"(常時アクティブに設定しました)":""}`}]};let i;try{i=await this.childManager.activateExternal(t,n)}catch(e){return log(`[activate] Failed "${t}": ${e.message}`),{content:[{type:"text",text:`❌ "${t}" の起動に失敗しました: ${e.message}`}],isError:!0}}const a=i.map(e=>e.name).join(", "),c=r?" 🔒常時アクティブ":"";log(`[activate] "${t}" → ${i.length} tools${r?" (alwaysOn)":""}. Sending list_changed.`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[activate] notification error: ${e.message}`)}return{content:[{type:"text",text:`✅ "${n.name}" を有効化しました${c}(ツール${i.length}個追加)\n\n追加ツール: ${a||"(なし)"}`}]}}if("deactivate_service"===r){const{service_id:e}=s,t=loadRegistry(),r=t[e];return r?r.alwaysOn?(t[e].alwaysOn=!1,saveRegistry(t),log(`[deactivate] "${e}" set alwaysOn: false`),{content:[{type:"text",text:`✅ "${e}" の常時アクティブを解除しました。次回起動時から自動有効化されません。`}]}):{content:[{type:"text",text:`ℹ️ "${e}" は常時アクティブではありません。変更の必要はありません。`}]}:{content:[{type:"text",text:`❌ サービス "${e}" は登録されていません。`}],isError:!0}}if("register_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,name:r,description:o,tags:n,command:i,args:a,env:c}=s,l=loadRegistry(),p=!!l[t];return l[t]={name:r,description:o,tags:n||[],type:"external",command:i||null,args:a||[],env:c||{}},saveRegistry(l),log(`[register] "${t}" ${p?"updated":"created"}`),{content:[{type:"text",text:`✅ サービス "${r}" を${p?"更新":"登録"}しました (id: ${t})\n\nタグ: ${(n||[]).join(", ")}\n\nsearch_mcp で検索可能になりました。`}]}}if("browse_mcp_store"===r){const{category:e}=s||{};log(`[browse_mcp_store] category=${e||"all"}`);try{const t=e?{category:e}:{},r=await listAvailableMcps(t,TOKEN,LANGUAGE);if(!r||!r.success)return{content:[{type:"text",text:`❌ MCPカタログの取得に失敗: ${r?.error||"Unknown"}`}],isError:!0};const s=r.data||[];if(0===s.length)return{content:[{type:"text",text:"📦 インストール可能なMCPはありません。"}]};let o=`📦 MCPストア一覧 (${s.length}件)\n\n`;return s.forEach(e=>{const t=e.hasAccess?"✅":"🔒",r=e.isOfficial?" [公式]":"";o+=`${t} **${e.name}**${r}\n`,o+=` ID: ${e.mcpId}\n`,o+=` ${e.description}\n`,o+=` カテゴリ: ${e.category} | ツール数: ${e.toolCount}\n\n`}),o+='\nインストールするには: install_from_store(mcp_id: "MCP_ID")',{content:[{type:"text",text:o}]}}catch(e){return log(`[browse_mcp_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}if("install_from_store"===r){const{mcp_id:e}=s;log(`[install_from_store] mcp_id=${e}`);try{const t=await downloadMcpPackage(e,TOKEN);if(!t||!t.success)return{content:[{type:"text",text:`❌ MCPダウンロードに失敗: ${t?.error||"Unknown"}`}],isError:!0};const r=t.data,s=r.files||[];if(0===s.length)return{content:[{type:"text",text:"❌ MCPパッケージにファイルが含まれていません。"}],isError:!0};const o=path.join(os.homedir(),".claude","mcp-servers",e);fs.mkdirSync(o,{recursive:!0});for(const e of s){const t=path.join(o,e.path);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(t,e.content,"utf-8")}log(`[install_from_store] ${s.length} files written to ${o}`);const n=r.packageSource||{};if(n.needsNpmInstall&&fs.existsSync(path.join(o,"package.json"))){const{execSync:e}=require("child_process");try{e("npm install --production",{cwd:o,timeout:6e4,stdio:"pipe"})}catch(e){return{content:[{type:"text",text:`⚠️ ファイル展開完了、npm install失敗: ${e.message}`}]}}}const i=n.entryPoint||"lib/index.cjs",a=loadRegistry();a[e]={name:n.name||e,description:n.description||"",tags:n.tags||[],type:"external",command:"node",args:[path.join(o,i)],env:{}};const c=n.requiredEnvVars||[];for(const t of c)a[e].env[t]=process.env[t]||`<SET_YOUR_${t}>`;saveRegistry(a);let l=`✅ MCP「${n.name||e}」をインストールしました\n\n`;l+=`📁 インストール先: ${o}\n`,l+=`📄 ファイル数: ${s.length}\n`,l+="⚙️ registry.json に登録済み\n",n.installNotes&&(l+=`\n📝 ${n.installNotes}\n`);const p=c.filter(e=>!process.env[e]);return p.length>0&&(l+="\n⚠️ 以下の環境変数をregistry.jsonで設定してください:\n",p.forEach(e=>{l+=` - ${e}\n`})),l+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:l}]}}catch(e){return log(`[install_from_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}try{const{found:e,result:t}=await this.childManager.callTool(r,s);if(e)return t&&t.content?t:{content:[{type:"text",text:String(t)}]}}catch(e){return log(`[callTool] Error proxying "${r}": ${e.message}`),{content:[{type:"text",text:`❌ ツール実行エラー: ${e.message}`}],isError:!0}}return{content:[{type:"text",text:`❌ Unknown tool: ${r}`}],isError:!0}})}async autoActivate(){const e=loadRegistry();for(const[t,r]of Object.entries(e))if(r.autoActivate||r.alwaysOn){log(`[autoActivate] Activating "${t}" (${r.alwaysOn?"alwaysOn":"autoActivate"})...`);try{log(`[autoActivate] "${t}" ready: ${(await this.childManager.activateExternal(t,r)).length} tools`)}catch(e){log(`[autoActivate] "${t}" failed: ${e.message}`)}}}async run(){const e=new StdioServerTransport;if(await this.server.connect(e),log("[Gateway] BlueLamp Gateway v2.0.0 started."),await this.autoActivate(),this.childManager.getActiveServiceIds().length>0)try{await this.server.notification({method:"notifications/tools/list_changed"}),log("[Gateway] Initial list_changed sent after autoActivate.")}catch(e){log(`[Gateway] Initial list_changed error: ${e.message}`)}if(KEYWORD&&TOKEN){log(`[Gateway] Auto-loading knowledge for keyword: ${KEYWORD}`);const e=await getPromptFromPortal(KEYWORD,TOKEN,LANGUAGE);e&&e.success?log(`[Gateway] Auto-load successful: ${e.title}`):log(`[Gateway] Auto-load failed for keyword: ${KEYWORD}`)}}}const server=new BlueLampServer;server.run().catch(e=>{log(`[Gateway] Fatal: ${e.message}`),process.exit(1)});
|