bluelamp 6.3.9 → 6.4.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/i18n/en.json +1 -1
- package/lib/i18n/ja.json +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/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,getGatewayConfig:getGatewayConfig,searchMcpServices:searchMcpServices,installMcpService:installMcpService,setServiceAlwaysOn:setServiceAlwaysOn,uninstallMcpService:uninstallMcpService,updateMcpEnv:updateMcpEnv,callMcpToolProxy:callMcpToolProxy}=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");function log(e){const t=`[${(new Date).toISOString()}] ${e}\n`;fs.appendFileSync(LOG_FILE,t),logger.info(`[MCP Server] ${e}`)}fs.existsSync(LOG_DIR)||fs.mkdirSync(LOG_DIR,{recursive:!0}),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}`);const LEGACY_REGISTRY=path.join(os.homedir(),".bluelamp","registry.json");if(fs.existsSync(LEGACY_REGISTRY))try{fs.unlinkSync(LEGACY_REGISTRY),log(`[startup] Legacy registry.json removed: ${LEGACY_REGISTRY}`)}catch(e){log(`[startup] Failed to remove legacy registry.json: ${e.message}`)}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,service:t}),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:"restart_service",description:"MCPサービスのプロセスを再起動します。コード更新や設定変更の反映に使用します。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"}},required:["service_id"]}},{name:"unregister_service",description:"MCPサービスをレジストリから完全に削除します。プロセスも停止します。",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"]}},{name:"save_mcp_env",description:"MCPの環境変数(APIキー等)をサーバーに安全に保存します。ローカルには保存されません。次回起動時にサーバーから自動取得されます。解約時はアクセスできなくなります。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"MCP サービスID(例: heygen, gemini-image)"},env:{type:"object",description:'環境変数のキーと値(例: {"HEYGEN_API_KEY": "sk_xxx"})',additionalProperties:{type:"string"}}},required:["service_id","env"]}}];if(e.push(...this.childManager.getActiveTools()),this._cloudProxyTools)for(const[t,r]of this._cloudProxyTools)e.push({name:t,description:r.description||`[${r.mcpId}] ${r.toolName}`,inputSchema:r.inputSchema||{type:"object",properties:{}}});return 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};if(!TOKEN)return{content:[{type:"text",text:"❌ 未認証です。MCPカタログの検索にはログインが必要です。"}],isError:!0};let t=[];try{const e=await searchMcpServices(s.query,TOKEN);if(!e.success)return{content:[{type:"text",text:`❌ MCPカタログ検索に失敗: ${e.error||"Unknown"}`}],isError:!0};t=(e.data||[]).map(e=>({id:e.mcp_id,service:{name:e.name,description:e.description,tags:[],alwaysOn:e.alwaysOn||!1},isInstalled:e.isInstalled||!1}))}catch(e){return log(`[search_mcp] Server search failed: ${e.message}`),{content:[{type:"text",text:`❌ MCPカタログ検索に失敗: ${e.message}`}],isError:!0}}if(0===t.length)return{content:[{type:"text",text:`🔍 「${s.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、browse_mcp_storeでカタログを確認してください。`}]};let r=`🔍 「${s.query}」の検索結果(${t.length}件)\n\n`;for(const e of t.slice(0,10)){const t=e.id,s=e.service,o=this.childManager.isActive(t),n=s.alwaysOn,i=e.isInstalled?"":" [未インストール]",c=n?" [常時アクティブ]":"";r+=`${n?"🔒":o?"🟢":"⚪"} **${s.name}**${c}${i} (id: \`${t}\`)\n`,r+=` ${s.description}\n\n`}return r+="有効化: activate_service | インストール: install_from_store",{content:[{type:"text",text:r}]}}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=await getGatewayConfig(TOKEN);if(!o.success)return{content:[{type:"text",text:`❌ サーバー設定の取得に失敗: ${o.error||"Unknown"}`}],isError:!0};const n=(o.data?.services||[]).find(e=>e.mcpId===t);if(!n)return{content:[{type:"text",text:`❌ サービス "${t}" はインストールされていません。install_from_storeでインストールしてください。`}],isError:!0};if(r&&(setServiceAlwaysOn(t,!0,TOKEN).catch(e=>log(`[activate] Server sync failed: ${e.message}`)),log(`[activate] "${t}" set alwaysOn: true`)),this.childManager.isActive(t))return{content:[{type:"text",text:`ℹ️ "${t}" は既に有効です。${r?"(常時アクティブに設定しました)":""}`}]};if("cloud_proxy"===n.hostingType){const e=Array.isArray(n.tools)?n.tools:[];if(0===e.length)return{content:[{type:"text",text:`❌ "${t}" (cloud_proxy) にツール定義がありません。`}],isError:!0};this._cloudProxyTools=this._cloudProxyTools||new Map;for(const r of e){const e=`${t}__${r.name}`;this._cloudProxyTools.set(e,{mcpId:t,toolName:r.name,description:r.description,inputSchema:r.inputSchema})}log(`[activate] "${t}" (cloud_proxy) → ${e.length} proxy tools registered`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[activate] notification error: ${e.message}`)}const s=e.map(e=>`${t}__${e.name}`).join(", "),o=r?" 🔒常時アクティブ":"";return{content:[{type:"text",text:`✅ "${n.name}" を有効化しました${o}(cloud_proxy ツール${e.length}個追加)\n\n追加ツール: ${s}`}]}}const i=path.join(os.homedir(),".claude","mcp-servers",t),c=n.entryPoint||"dist/index.js",a={name:n.name,description:n.description,type:"external",command:n.command||"node",args:n.args&&n.args.length?n.args:[path.join(i,c)],env:n.env||{}};let l;try{l=await this.childManager.activateExternal(t,a)}catch(e){return log(`[activate] Failed "${t}": ${e.message}`),{content:[{type:"text",text:`❌ "${t}" の起動に失敗しました: ${e.message}`}],isError:!0}}const p=l.map(e=>e.name).join(", "),d=r?" 🔒常時アクティブ":"";log(`[activate] "${t}" → ${l.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:`✅ "${a.name}" を有効化しました${d}(ツール${l.length}個追加)\n\n追加ツール: ${p||"(なし)"}`}]}}if("deactivate_service"===r){const{service_id:e}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};const t=await setServiceAlwaysOn(e,!1,TOKEN);if(!t.success)return{content:[{type:"text",text:`❌ "${e}" の解除に失敗: ${t.error||"Unknown"}`}],isError:!0};log(`[deactivate] "${e}" set alwaysOn: false`);let r=0;if(this._cloudProxyTools){const t=`${e}__`;for(const e of[...this._cloudProxyTools.keys()])e.startsWith(t)&&(this._cloudProxyTools.delete(e),r++);r>0&&log(`[deactivate] "${e}" (cloud_proxy) → ${r} proxy tools removed`)}let o=!1;if(this.childManager.isActive(e)&&(this.childManager.deactivate(e),log(`[deactivate] "${e}" child process stopped`),o=!0),r>0||o)try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[deactivate] notification error: ${e.message}`)}let n=`✅ "${e}" の常時アクティブを解除しました。`;return r>0&&(n+=` cloud_proxy ツール${r}個を削除しました。`),o&&(n+=" 子プロセスを停止しました。"),n+=" 次回起動時から自動有効化されません。",{content:[{type:"text",text:n}]}}if("restart_service"===r){const{service_id:e}=s,t=this.childManager.children.get(e),r=t?.service;if(!r)return{content:[{type:"text",text:`❌ "${e}" は現在アクティブではありません。activate_service で有効化してください。`}],isError:!0};let o,n=r;if(TOKEN)try{const t=await getGatewayConfig(TOKEN);if(t.success){const s=(t.data?.services||[]).find(t=>t.mcpId===e);s&&s.env&&(n={...r,env:s.env},log(`[restart] "${e}" env refreshed from server (keys=${Object.keys(s.env).join(",")||"none"})`))}}catch(e){log(`[restart] env refresh failed (using cached): ${e.message}`)}this.childManager.deactivate(e),log(`[restart] "${e}" stopped`);try{o=await this.childManager.activateExternal(e,n)}catch(t){return log(`[restart] Failed "${e}": ${t.message}`),{content:[{type:"text",text:`❌ "${e}" の再起動に失敗しました: ${t.message}`}],isError:!0}}log(`[restart] "${e}" → ${o.length} tools. Sending list_changed.`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[restart] notification error: ${e.message}`)}const i=o.map(e=>e.name).join(", ");return{content:[{type:"text",text:`🔄 "${n.name}" を再起動しました(ツール${o.length}個)\n\nツール: ${i||"(なし)"}`}]}}if("unregister_service"===r){const{service_id:e}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};this.childManager.isActive(e)&&(this.childManager.deactivate(e),log(`[unregister] "${e}" stopped`));const t=await uninstallMcpService(e,TOKEN);if(!t.success)return{content:[{type:"text",text:`❌ "${e}" のサーバー削除に失敗: ${t.error||"Unknown"}`}],isError:!0};log(`[unregister] "${e}" uninstalled from server`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[unregister] notification error: ${e.message}`)}return{content:[{type:"text",text:`🗑️ "${e}" を削除しました。プロセスも停止済みです。`}]}}if("register_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,env:r}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};const o=r||{},n=await installMcpService(t,!1,TOKEN,o);return n.success?(log(`[register] "${t}" installed on server`),{content:[{type:"text",text:`✅ サービス "${t}" をサーバーに登録しました。\n\n次のステップ:\n- save_mcp_env で APIキー等を設定\n- activate_service で有効化`}]}):{content:[{type:"text",text:`❌ "${t}" の登録に失敗しました: ${n.error||"Unknown"}\n\n※ MCP はサーバーカタログに登録されている必要があります。browse_mcp_store で確認してください。`}],isError:!0}}if("save_mcp_env"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,env:r}=s;if(!t||!r||0===Object.keys(r).length)return{content:[{type:"text",text:"❌ service_id と env(キー/値のペア)が必要です"}],isError:!0};log(`[save_mcp_env] ${t}: ${Object.keys(r).join(", ")}`);try{const e=await updateMcpEnv(t,r,TOKEN);return e.success?{content:[{type:"text",text:`🔐 環境変数をサーバーに保存しました\n\nサービス: ${t}\n保存キー: ${Object.keys(r).join(", ")}\n\n※ ローカルには保存されません。次回起動時にサーバーから自動取得されます。`}]}:{content:[{type:"text",text:`❌ サーバー保存失敗: ${e.error}`}],isError:!0}}catch(e){return log(`[save_mcp_env] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}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.packageSource||{},o=r.files||[];let n=s.hostingType||s.hosting&&s.hosting.type||null;if(TOKEN){let t=!1;try{const r=await getGatewayConfig(TOKEN);if(r.success){const s=(r.data?.services||[]).find(t=>t.mcpId===e);s&&(t=!0===s.alwaysOn,n||(n=s.hostingType))}}catch(e){}const r=await installMcpService(e,t,TOKEN);if(!r.success)return{content:[{type:"text",text:`❌ サーバー登録失敗: ${r.error||"Unknown"}`}],isError:!0};log(`[install_from_store] Server sync OK: ${e} (alwaysOn=${t})`)}if("cloud_proxy"===n){log(`[install_from_store] "${e}" is cloud_proxy, skipping local extract`);let t=`✅ MCP「${s.name||e}」をインストールしました(cloud_proxy)\n\n`;return t+="☁ クラウド実行のためローカル展開は不要です\n",s.installNotes&&(t+=`\n📝 ${s.installNotes}\n`),t+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:t}]}}if(0===o.length)return{content:[{type:"text",text:"❌ MCPパッケージにファイルが含まれていません。"}],isError:!0};const i=path.join(os.homedir(),".claude","mcp-servers",e);fs.mkdirSync(i,{recursive:!0});for(const e of o){const t=path.join(i,e.path);if(fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(t,e.content,"utf-8"),"number"==typeof e.mode)try{fs.chmodSync(t,511&e.mode)}catch(e){}}if(log(`[install_from_store] ${o.length} files written to ${i}`),s.needsNpmInstall&&fs.existsSync(path.join(i,"package.json"))){const{execSync:e}=require("child_process");try{e("npm install --production",{cwd:i,timeout:6e4,stdio:"pipe"})}catch(e){return{content:[{type:"text",text:`⚠️ ファイル展開完了、npm install失敗: ${e.message}`}]}}}let c=`✅ MCP「${s.name||e}」をインストールしました\n\n`;c+=`📁 インストール先: ${i}\n`,c+=`📄 ファイル数: ${o.length}\n`,s.installNotes&&(c+=`\n📝 ${s.installNotes}\n`);const a=(s.requiredEnvVars||[]).filter(e=>!process.env[e]);return a.length>0&&(c+="\n⚠️ 以下の環境変数を save_mcp_env で設定してください:\n",a.forEach(e=>{c+=` - ${e}\n`})),c+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:c}]}}catch(e){return log(`[install_from_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}if(this._cloudProxyTools&&this._cloudProxyTools.has(r)){const e=this._cloudProxyTools.get(r);log(`[callTool] Cloud proxy: ${r} → ${e.mcpId}/${e.toolName}`);try{const t=await callMcpToolProxy(e.mcpId,e.toolName,s,TOKEN);return t.success&&t.data?t.data.content?t.data:{content:[{type:"text",text:JSON.stringify(t.data,null,2)}]}:{content:[{type:"text",text:`❌ プロキシエラー: ${t.error||"Unknown"}`}],isError:!0}}catch(e){return log(`[callTool] Cloud proxy error "${r}": ${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(){if(!TOKEN)return void log("[autoActivate] No token, skipping auto-activation");let e=[];try{const t=await getGatewayConfig(TOKEN);if(!t.success)return void log(`[autoActivate] Server config failed: ${t.error||"Unknown"}. Starting with no services.`);e=t.data?.services||[],log(`[autoActivate] Server config: ${e.length} services`)}catch(e){return void log(`[autoActivate] Server config error: ${e.message}. Starting with no services.`)}for(const t of e)if(t.alwaysOn)if("cloud_proxy"===t.hostingType){const e=Array.isArray(t.tools)?t.tools:[];if(0===e.length){log(`[autoActivate] "${t.mcpId}" (cloud_proxy) has no tools, skipping`);continue}for(const r of e){const e=`${t.mcpId}__${r.name}`;this._cloudProxyTools=this._cloudProxyTools||new Map,this._cloudProxyTools.set(e,{mcpId:t.mcpId,toolName:r.name,description:r.description,inputSchema:r.inputSchema})}log(`[autoActivate] "${t.mcpId}" (cloud_proxy) → ${e.length} proxy tools registered`)}else{const e=path.join(os.homedir(),".claude","mcp-servers",t.mcpId),r=t.entryPoint||"dist/index.js",s={name:t.name,description:t.description,type:"external",command:t.command||"node",args:t.args&&t.args.length?t.args:[path.join(e,r)],env:t.env||{}};log(`[autoActivate] Activating "${t.mcpId}" (${t.hostingType})...`);try{const e=await this.childManager.activateExternal(t.mcpId,s);log(`[autoActivate] "${t.mcpId}" ready: ${e.length} tools`)}catch(e){log(`[autoActivate] "${t.mcpId}" 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;let _blIsShuttingDown=!1;const _blCleanupChildren=e=>{if(!_blIsShuttingDown){_blIsShuttingDown=!0;try{log(`[Gateway] Shutdown (${e}) - cleaning up children...`)}catch(e){}if(server&&server.childManager&&server.childManager.children)for(const[e,t]of server.childManager.children)try{try{log(`[Gateway] Stopping child: ${e}`)}catch(e){}t&&"external"===t.type&&t.process&&"function"==typeof t.process.stop&&t.process.stop()}catch(e){}setTimeout(()=>process.exit(0),200).unref()}};process.on("SIGTERM",()=>_blCleanupChildren("SIGTERM")),process.on("SIGINT",()=>_blCleanupChildren("SIGINT")),process.on("SIGHUP",()=>_blCleanupChildren("SIGHUP")),process.stdin.on("end",()=>_blCleanupChildren("stdin-end")),process.stdin.on("close",()=>_blCleanupChildren("stdin-close")),process.on("exit",()=>{if(server&&server.childManager&&server.childManager.children)for(const[e,t]of server.childManager.children)try{t&&t.process&&t.process.process&&"function"==typeof t.process.process.kill&&t.process.process.kill("SIGKILL")}catch(e){}}),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,getGatewayConfig:getGatewayConfig,searchMcpServices:searchMcpServices,installMcpService:installMcpService,setServiceAlwaysOn:setServiceAlwaysOn,uninstallMcpService:uninstallMcpService,updateMcpEnv:updateMcpEnv,callMcpToolProxy:callMcpToolProxy}=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-要件定義",{TokenManager:TokenManager}=require("../auth/TokenManager"),TOKEN=TokenManager.loadTokenStringSync(),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");function log(e){const t=`[${(new Date).toISOString()}] ${e}\n`;fs.appendFileSync(LOG_FILE,t),logger.info(`[MCP Server] ${e}`)}fs.existsSync(LOG_DIR)||fs.mkdirSync(LOG_DIR,{recursive:!0}),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}`);const LEGACY_REGISTRY=path.join(os.homedir(),".bluelamp","registry.json");if(fs.existsSync(LEGACY_REGISTRY))try{fs.unlinkSync(LEGACY_REGISTRY),log(`[startup] Legacy registry.json removed: ${LEGACY_REGISTRY}`)}catch(e){log(`[startup] Failed to remove legacy registry.json: ${e.message}`)}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,service:t}),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:"BlueLamp にログインしてください。ターミナルで `bluelamp1` を一度実行すると ~/.musuhi/portal-token.enc が作成されます。"}}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:"restart_service",description:"MCPサービスのプロセスを再起動します。コード更新や設定変更の反映に使用します。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"サービスID"}},required:["service_id"]}},{name:"unregister_service",description:"MCPサービスをレジストリから完全に削除します。プロセスも停止します。",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"]}},{name:"save_mcp_env",description:"MCPの環境変数(APIキー等)をサーバーに安全に保存します。ローカルには保存されません。次回起動時にサーバーから自動取得されます。解約時はアクセスできなくなります。",inputSchema:{type:"object",properties:{service_id:{type:"string",description:"MCP サービスID(例: heygen, gemini-image)"},env:{type:"object",description:'環境変数のキーと値(例: {"HEYGEN_API_KEY": "sk_xxx"})',additionalProperties:{type:"string"}}},required:["service_id","env"]}}];if(e.push(...this.childManager.getActiveTools()),this._cloudProxyTools)for(const[t,r]of this._cloudProxyTools)e.push({name:t,description:r.description||`[${r.mcpId}] ${r.toolName}`,inputSchema:r.inputSchema||{type:"object",properties:{}}});return 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};if(!TOKEN)return{content:[{type:"text",text:"❌ 未認証です。MCPカタログの検索にはログインが必要です。"}],isError:!0};let t=[];try{const e=await searchMcpServices(s.query,TOKEN);if(!e.success)return{content:[{type:"text",text:`❌ MCPカタログ検索に失敗: ${e.error||"Unknown"}`}],isError:!0};t=(e.data||[]).map(e=>({id:e.mcp_id,service:{name:e.name,description:e.description,tags:[],alwaysOn:e.alwaysOn||!1},isInstalled:e.isInstalled||!1}))}catch(e){return log(`[search_mcp] Server search failed: ${e.message}`),{content:[{type:"text",text:`❌ MCPカタログ検索に失敗: ${e.message}`}],isError:!0}}if(0===t.length)return{content:[{type:"text",text:`🔍 「${s.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、browse_mcp_storeでカタログを確認してください。`}]};let r=`🔍 「${s.query}」の検索結果(${t.length}件)\n\n`;for(const e of t.slice(0,10)){const t=e.id,s=e.service,o=this.childManager.isActive(t),n=s.alwaysOn,i=e.isInstalled?"":" [未インストール]",c=n?" [常時アクティブ]":"";r+=`${n?"🔒":o?"🟢":"⚪"} **${s.name}**${c}${i} (id: \`${t}\`)\n`,r+=` ${s.description}\n\n`}return r+="有効化: activate_service | インストール: install_from_store",{content:[{type:"text",text:r}]}}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=await getGatewayConfig(TOKEN);if(!o.success)return{content:[{type:"text",text:`❌ サーバー設定の取得に失敗: ${o.error||"Unknown"}`}],isError:!0};const n=(o.data?.services||[]).find(e=>e.mcpId===t);if(!n)return{content:[{type:"text",text:`❌ サービス "${t}" はインストールされていません。install_from_storeでインストールしてください。`}],isError:!0};if(r&&(setServiceAlwaysOn(t,!0,TOKEN).catch(e=>log(`[activate] Server sync failed: ${e.message}`)),log(`[activate] "${t}" set alwaysOn: true`)),this.childManager.isActive(t))return{content:[{type:"text",text:`ℹ️ "${t}" は既に有効です。${r?"(常時アクティブに設定しました)":""}`}]};if("cloud_proxy"===n.hostingType){const e=Array.isArray(n.tools)?n.tools:[];if(0===e.length)return{content:[{type:"text",text:`❌ "${t}" (cloud_proxy) にツール定義がありません。`}],isError:!0};this._cloudProxyTools=this._cloudProxyTools||new Map;for(const r of e){const e=`${t}__${r.name}`;this._cloudProxyTools.set(e,{mcpId:t,toolName:r.name,description:r.description,inputSchema:r.inputSchema})}log(`[activate] "${t}" (cloud_proxy) → ${e.length} proxy tools registered`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[activate] notification error: ${e.message}`)}const s=e.map(e=>`${t}__${e.name}`).join(", "),o=r?" 🔒常時アクティブ":"";return{content:[{type:"text",text:`✅ "${n.name}" を有効化しました${o}(cloud_proxy ツール${e.length}個追加)\n\n追加ツール: ${s}`}]}}const i=path.join(os.homedir(),".claude","mcp-servers",t),c=n.entryPoint||"dist/index.js",a={name:n.name,description:n.description,type:"external",command:n.command||"node",args:n.args&&n.args.length?n.args:[path.join(i,c)],env:n.env||{}};let l;try{l=await this.childManager.activateExternal(t,a)}catch(e){return log(`[activate] Failed "${t}": ${e.message}`),{content:[{type:"text",text:`❌ "${t}" の起動に失敗しました: ${e.message}`}],isError:!0}}const p=l.map(e=>e.name).join(", "),d=r?" 🔒常時アクティブ":"";log(`[activate] "${t}" → ${l.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:`✅ "${a.name}" を有効化しました${d}(ツール${l.length}個追加)\n\n追加ツール: ${p||"(なし)"}`}]}}if("deactivate_service"===r){const{service_id:e}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};const t=await setServiceAlwaysOn(e,!1,TOKEN);if(!t.success)return{content:[{type:"text",text:`❌ "${e}" の解除に失敗: ${t.error||"Unknown"}`}],isError:!0};log(`[deactivate] "${e}" set alwaysOn: false`);let r=0;if(this._cloudProxyTools){const t=`${e}__`;for(const e of[...this._cloudProxyTools.keys()])e.startsWith(t)&&(this._cloudProxyTools.delete(e),r++);r>0&&log(`[deactivate] "${e}" (cloud_proxy) → ${r} proxy tools removed`)}let o=!1;if(this.childManager.isActive(e)&&(this.childManager.deactivate(e),log(`[deactivate] "${e}" child process stopped`),o=!0),r>0||o)try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[deactivate] notification error: ${e.message}`)}let n=`✅ "${e}" の常時アクティブを解除しました。`;return r>0&&(n+=` cloud_proxy ツール${r}個を削除しました。`),o&&(n+=" 子プロセスを停止しました。"),n+=" 次回起動時から自動有効化されません。",{content:[{type:"text",text:n}]}}if("restart_service"===r){const{service_id:e}=s,t=this.childManager.children.get(e),r=t?.service;if(!r)return{content:[{type:"text",text:`❌ "${e}" は現在アクティブではありません。activate_service で有効化してください。`}],isError:!0};let o,n=r;if(TOKEN)try{const t=await getGatewayConfig(TOKEN);if(t.success){const s=(t.data?.services||[]).find(t=>t.mcpId===e);s&&s.env&&(n={...r,env:s.env},log(`[restart] "${e}" env refreshed from server (keys=${Object.keys(s.env).join(",")||"none"})`))}}catch(e){log(`[restart] env refresh failed (using cached): ${e.message}`)}this.childManager.deactivate(e),log(`[restart] "${e}" stopped`);try{o=await this.childManager.activateExternal(e,n)}catch(t){return log(`[restart] Failed "${e}": ${t.message}`),{content:[{type:"text",text:`❌ "${e}" の再起動に失敗しました: ${t.message}`}],isError:!0}}log(`[restart] "${e}" → ${o.length} tools. Sending list_changed.`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[restart] notification error: ${e.message}`)}const i=o.map(e=>e.name).join(", ");return{content:[{type:"text",text:`🔄 "${n.name}" を再起動しました(ツール${o.length}個)\n\nツール: ${i||"(なし)"}`}]}}if("unregister_service"===r){const{service_id:e}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};this.childManager.isActive(e)&&(this.childManager.deactivate(e),log(`[unregister] "${e}" stopped`));const t=await uninstallMcpService(e,TOKEN);if(!t.success)return{content:[{type:"text",text:`❌ "${e}" のサーバー削除に失敗: ${t.error||"Unknown"}`}],isError:!0};log(`[unregister] "${e}" uninstalled from server`);try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[unregister] notification error: ${e.message}`)}return{content:[{type:"text",text:`🗑️ "${e}" を削除しました。プロセスも停止済みです。`}]}}if("register_service"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,env:r}=s;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};const o=r||{},n=await installMcpService(t,!1,TOKEN,o);return n.success?(log(`[register] "${t}" installed on server`),{content:[{type:"text",text:`✅ サービス "${t}" をサーバーに登録しました。\n\n次のステップ:\n- save_mcp_env で APIキー等を設定\n- activate_service で有効化`}]}):{content:[{type:"text",text:`❌ "${t}" の登録に失敗しました: ${n.error||"Unknown"}\n\n※ MCP はサーバーカタログに登録されている必要があります。browse_mcp_store で確認してください。`}],isError:!0}}if("save_mcp_env"===r){const e=this.checkAuth();if(!e.ok)return{content:[{type:"text",text:`🔒 ${e.error}`}],isError:!0};const{service_id:t,env:r}=s;if(!t||!r||0===Object.keys(r).length)return{content:[{type:"text",text:"❌ service_id と env(キー/値のペア)が必要です"}],isError:!0};log(`[save_mcp_env] ${t}: ${Object.keys(r).join(", ")}`);try{const e=await updateMcpEnv(t,r,TOKEN);return e.success?{content:[{type:"text",text:`🔐 環境変数をサーバーに保存しました\n\nサービス: ${t}\n保存キー: ${Object.keys(r).join(", ")}\n\n※ ローカルには保存されません。次回起動時にサーバーから自動取得されます。`}]}:{content:[{type:"text",text:`❌ サーバー保存失敗: ${e.error}`}],isError:!0}}catch(e){return log(`[save_mcp_env] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}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.packageSource||{},o=r.files||[];let n=s.hostingType||s.hosting&&s.hosting.type||null;if(TOKEN){let t=!1;try{const r=await getGatewayConfig(TOKEN);if(r.success){const s=(r.data?.services||[]).find(t=>t.mcpId===e);s&&(t=!0===s.alwaysOn,n||(n=s.hostingType))}}catch(e){}const r=await installMcpService(e,t,TOKEN);if(!r.success)return{content:[{type:"text",text:`❌ サーバー登録失敗: ${r.error||"Unknown"}`}],isError:!0};log(`[install_from_store] Server sync OK: ${e} (alwaysOn=${t})`)}if("cloud_proxy"===n){log(`[install_from_store] "${e}" is cloud_proxy, skipping local extract`);let t=`✅ MCP「${s.name||e}」をインストールしました(cloud_proxy)\n\n`;return t+="☁ クラウド実行のためローカル展開は不要です\n",s.installNotes&&(t+=`\n📝 ${s.installNotes}\n`),t+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:t}]}}if(0===o.length)return{content:[{type:"text",text:"❌ MCPパッケージにファイルが含まれていません。"}],isError:!0};const i=path.join(os.homedir(),".claude","mcp-servers",e);fs.mkdirSync(i,{recursive:!0});for(const e of o){const t=path.join(i,e.path);if(fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.writeFileSync(t,e.content,"utf-8"),"number"==typeof e.mode)try{fs.chmodSync(t,511&e.mode)}catch(e){}}if(log(`[install_from_store] ${o.length} files written to ${i}`),s.needsNpmInstall&&fs.existsSync(path.join(i,"package.json"))){const{execSync:e}=require("child_process");try{e("npm install --production",{cwd:i,timeout:6e4,stdio:"pipe"})}catch(e){return{content:[{type:"text",text:`⚠️ ファイル展開完了、npm install失敗: ${e.message}`}]}}}let c=`✅ MCP「${s.name||e}」をインストールしました\n\n`;c+=`📁 インストール先: ${i}\n`,c+=`📄 ファイル数: ${o.length}\n`,s.installNotes&&(c+=`\n📝 ${s.installNotes}\n`);const a=(s.requiredEnvVars||[]).filter(e=>!process.env[e]);return a.length>0&&(c+="\n⚠️ 以下の環境変数を save_mcp_env で設定してください:\n",a.forEach(e=>{c+=` - ${e}\n`})),c+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:c}]}}catch(e){return log(`[install_from_store] error: ${e.message}`),{content:[{type:"text",text:`❌ ${e.message}`}],isError:!0}}}if(this._cloudProxyTools&&this._cloudProxyTools.has(r)){const e=this._cloudProxyTools.get(r);log(`[callTool] Cloud proxy: ${r} → ${e.mcpId}/${e.toolName}`);try{const t=await callMcpToolProxy(e.mcpId,e.toolName,s,TOKEN);return t.success&&t.data?t.data.content?t.data:{content:[{type:"text",text:JSON.stringify(t.data,null,2)}]}:{content:[{type:"text",text:`❌ プロキシエラー: ${t.error||"Unknown"}`}],isError:!0}}catch(e){return log(`[callTool] Cloud proxy error "${r}": ${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(){if(!TOKEN)return void log("[autoActivate] No token, skipping auto-activation");let e=[];try{const t=await getGatewayConfig(TOKEN);if(!t.success)return void log(`[autoActivate] Server config failed: ${t.error||"Unknown"}. Starting with no services.`);e=t.data?.services||[],log(`[autoActivate] Server config: ${e.length} services`)}catch(e){return void log(`[autoActivate] Server config error: ${e.message}. Starting with no services.`)}for(const t of e)if(t.alwaysOn)if("cloud_proxy"===t.hostingType){const e=Array.isArray(t.tools)?t.tools:[];if(0===e.length){log(`[autoActivate] "${t.mcpId}" (cloud_proxy) has no tools, skipping`);continue}for(const r of e){const e=`${t.mcpId}__${r.name}`;this._cloudProxyTools=this._cloudProxyTools||new Map,this._cloudProxyTools.set(e,{mcpId:t.mcpId,toolName:r.name,description:r.description,inputSchema:r.inputSchema})}log(`[autoActivate] "${t.mcpId}" (cloud_proxy) → ${e.length} proxy tools registered`)}else{const e=path.join(os.homedir(),".claude","mcp-servers",t.mcpId),r=t.entryPoint||"dist/index.js",s={name:t.name,description:t.description,type:"external",command:t.command||"node",args:t.args&&t.args.length?t.args:[path.join(e,r)],env:t.env||{}};log(`[autoActivate] Activating "${t.mcpId}" (${t.hostingType})...`);try{const e=await this.childManager.activateExternal(t.mcpId,s);log(`[autoActivate] "${t.mcpId}" ready: ${e.length} tools`)}catch(e){log(`[autoActivate] "${t.mcpId}" 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;let _blIsShuttingDown=!1;const _blCleanupChildren=e=>{if(!_blIsShuttingDown){_blIsShuttingDown=!0;try{log(`[Gateway] Shutdown (${e}) - cleaning up children...`)}catch(e){}if(server&&server.childManager&&server.childManager.children)for(const[e,t]of server.childManager.children)try{try{log(`[Gateway] Stopping child: ${e}`)}catch(e){}t&&"external"===t.type&&t.process&&"function"==typeof t.process.stop&&t.process.stop()}catch(e){}setTimeout(()=>process.exit(0),200).unref()}};process.on("SIGTERM",()=>_blCleanupChildren("SIGTERM")),process.on("SIGINT",()=>_blCleanupChildren("SIGINT")),process.on("SIGHUP",()=>_blCleanupChildren("SIGHUP")),process.stdin.on("end",()=>_blCleanupChildren("stdin-end")),process.stdin.on("close",()=>_blCleanupChildren("stdin-close")),process.on("exit",()=>{if(server&&server.childManager&&server.childManager.children)for(const[e,t]of server.childManager.children)try{t&&t.process&&t.process.process&&"function"==typeof t.process.process.kill&&t.process.process.kill("SIGKILL")}catch(e){}}),server.run().catch(e=>{log(`[Gateway] Fatal: ${e.message}`),process.exit(1)});
|