bluelamp 6.3.3 → 6.3.5
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/README.md +14 -0
- package/bin/bluelamp-vscode-base +310 -1
- package/lib/auth/InteractiveLogin.js +253 -1
- package/lib/auth/PortalAuthClient.js +186 -1
- package/lib/auth/TokenManager.js +258 -1
- package/lib/i18n/index.js +75 -1
- package/lib/mcp-server/index.cjs +1128 -1
- package/lib/mcp-server/portal-api-client.cjs +934 -1
- package/lib/utils/logger.js +83 -1
- package/package.json +22 -8
- package/scripts/build.js +354 -0
- package/scripts/generate-commands.js +44 -1
- package/scripts/setup-mcps.js +107 -1
- package/lib/mcp-gmail/index.js +0 -638
package/lib/mcp-server/index.cjs
CHANGED
|
@@ -1,2 +1,1129 @@
|
|
|
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,o){this.serviceId=e,this.command=t,this.args=(r||[]).map(e=>"string"==typeof e?e.replace(/^~\//,os.homedir()+"/"):e),this.env={...process.env,...o},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 o=t.tools||[];this.tools=o.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,o)=>{const s=++this.requestId,i=JSON.stringify({jsonrpc:"2.0",id:s,method:e,params:t})+"\n";this.pending.set(s,{resolve:r,reject:o});try{this.process.stdin.write(i)}catch(e){this.pending.delete(s),o(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||{}),o=await r.start();return this.children.set(e,{type:"external",process:r,tools:o,service:t}),o}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,o]of this.children)if("external"===o.type){const r=o.tools.find(t=>t.name===e);if(r)return{found:!0,result:await o.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:o}=e.params;if(log(`[CallTool] ${r}`),"load_knowledge"===r){const{keyword:e}=o;log(`[load_knowledge] keyword: ${e}`);try{const r=await getPromptFromPortal(e,TOKEN,LANGUAGE);if(!r||!r.success){const o=r?.error||t("mcp.knowledgeNotFound",{keyword:e});return{content:[{type:"text",text:`❌ ${t("mcp.portalApiFailed",{error:o})}`}],isError:!0}}const o="ja"!==LANGUAGE?"\n\n[Note] The user's preferred language is English.":"";return{content:[{type:"text",text:`✅ ${t("mcp.knowledgeLoadSuccess")}\n\n${r.knowledge}${o}\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(o.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:`🔍 「${o.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、browse_mcp_storeでカタログを確認してください。`}]};let r=`🔍 「${o.query}」の検索結果(${t.length}件)\n\n`;for(const e of t.slice(0,10)){const t=e.id,o=e.service,s=this.childManager.isActive(t),i=o.alwaysOn,n=e.isInstalled?"":" [未インストール]",c=i?" [常時アクティブ]":"";r+=`${i?"🔒":s?"🟢":"⚪"} **${o.name}**${c}${n} (id: \`${t}\`)\n`,r+=` ${o.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}=o,s=await getGatewayConfig(TOKEN);if(!s.success)return{content:[{type:"text",text:`❌ サーバー設定の取得に失敗: ${s.error||"Unknown"}`}],isError:!0};const i=(s.data?.services||[]).find(e=>e.mcpId===t);if(!i)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"===i.hostingType){const e=Array.isArray(i.tools)?i.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 o=e.map(e=>`${t}__${e.name}`).join(", "),s=r?" 🔒常時アクティブ":"";return{content:[{type:"text",text:`✅ "${i.name}" を有効化しました${s}(cloud_proxy ツール${e.length}個追加)\n\n追加ツール: ${o}`}]}}const n=path.join(os.homedir(),".claude","mcp-servers",t),c=i.entryPoint||"dist/index.js",a={name:i.name,description:i.description,type:"external",command:i.command||"node",args:i.args&&i.args.length?i.args:[path.join(n,c)],env:i.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}=o;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 s=!1;if(this.childManager.isActive(e)&&(this.childManager.deactivate(e),log(`[deactivate] "${e}" child process stopped`),s=!0),r>0||s)try{await this.server.notification({method:"notifications/tools/list_changed"})}catch(e){log(`[deactivate] notification error: ${e.message}`)}let i=`✅ "${e}" の常時アクティブを解除しました。`;return r>0&&(i+=` cloud_proxy ツール${r}個を削除しました。`),s&&(i+=" 子プロセスを停止しました。"),i+=" 次回起動時から自動有効化されません。",{content:[{type:"text",text:i}]}}if("restart_service"===r){const{service_id:e}=o,t=this.childManager.children.get(e),r=t?.service;if(!r)return{content:[{type:"text",text:`❌ "${e}" は現在アクティブではありません。activate_service で有効化してください。`}],isError:!0};let s;this.childManager.deactivate(e),log(`[restart] "${e}" stopped`);try{s=await this.childManager.activateExternal(e,r)}catch(t){return log(`[restart] Failed "${e}": ${t.message}`),{content:[{type:"text",text:`❌ "${e}" の再起動に失敗しました: ${t.message}`}],isError:!0}}log(`[restart] "${e}" → ${s.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=s.map(e=>e.name).join(", ");return{content:[{type:"text",text:`🔄 "${r.name}" を再起動しました(ツール${s.length}個)\n\nツール: ${i||"(なし)"}`}]}}if("unregister_service"===r){const{service_id:e}=o;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}=o;if(!TOKEN)return{content:[{type:"text",text:"🔒 未認証です。ログインが必要です。"}],isError:!0};const s=r||{},i=await installMcpService(t,!1,TOKEN,s);return i.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}" の登録に失敗しました: ${i.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}=o;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}=o||{};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 o=r.data||[];if(0===o.length)return{content:[{type:"text",text:"📦 インストール可能なMCPはありません。"}]};let s=`📦 MCPストア一覧 (${o.length}件)\n\n`;return o.forEach(e=>{const t=e.hasAccess?"✅":"🔒",r=e.isOfficial?" [公式]":"";s+=`${t} **${e.name}**${r}\n`,s+=` ID: ${e.mcpId}\n`,s+=` ${e.description}\n`,s+=` カテゴリ: ${e.category} | ツール数: ${e.toolCount}\n\n`}),s+='\nインストールするには: install_from_store(mcp_id: "MCP_ID")',{content:[{type:"text",text:s}]}}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}=o;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,o=r.packageSource||{},s=r.files||[];let i=o.hostingType||o.hosting&&o.hosting.type||null;if(TOKEN){const t=await installMcpService(e,!1,TOKEN);if(!t.success)return{content:[{type:"text",text:`❌ サーバー登録失敗: ${t.error||"Unknown"}`}],isError:!0};if(log(`[install_from_store] Server sync OK: ${e}`),!i){const t=await getGatewayConfig(TOKEN);if(t.success){const r=(t.data?.services||[]).find(t=>t.mcpId===e);r&&(i=r.hostingType)}}}if("cloud_proxy"===i){log(`[install_from_store] "${e}" is cloud_proxy, skipping local extract`);let t=`✅ MCP「${o.name||e}」をインストールしました(cloud_proxy)\n\n`;return t+="☁ クラウド実行のためローカル展開は不要です\n",o.installNotes&&(t+=`\n📝 ${o.installNotes}\n`),t+=`\nactivate_service(service_id: "${e}") で有効化できます。`,{content:[{type:"text",text:t}]}}if(0===s.length)return{content:[{type:"text",text:"❌ MCPパッケージにファイルが含まれていません。"}],isError:!0};const n=path.join(os.homedir(),".claude","mcp-servers",e);fs.mkdirSync(n,{recursive:!0});for(const e of s){const t=path.join(n,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] ${s.length} files written to ${n}`),o.needsNpmInstall&&fs.existsSync(path.join(n,"package.json"))){const{execSync:e}=require("child_process");try{e("npm install --production",{cwd:n,timeout:6e4,stdio:"pipe"})}catch(e){return{content:[{type:"text",text:`⚠️ ファイル展開完了、npm install失敗: ${e.message}`}]}}}let c=`✅ MCP「${o.name||e}」をインストールしました\n\n`;c+=`📁 インストール先: ${n}\n`,c+=`📄 ファイル数: ${s.length}\n`,o.installNotes&&(c+=`\n📝 ${o.installNotes}\n`);const a=(o.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,o,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,o);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",o={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,o);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;server.run().catch(e=>{log(`[Gateway] Fatal: ${e.message}`),process.exit(1)});
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BlueLamp MCP Server with Gateway
|
|
5
|
+
* Claude Code専用のMCPサーバー + MCPゲートウェイ統合実装
|
|
6
|
+
*
|
|
7
|
+
* 常時ツール:
|
|
8
|
+
* - load_knowledge: プロジェクト知識読み込み
|
|
9
|
+
* - search_mcp: ローカルregistry検索
|
|
10
|
+
* - activate_service: サービス有効化 → tools/list_changed
|
|
11
|
+
* - deactivate_service: サービス無効化 → tools/list_changed
|
|
12
|
+
* - register_service: 新サービス登録
|
|
13
|
+
*
|
|
14
|
+
* autoActivate:
|
|
15
|
+
* - playwright, password-manager(起動時に自動有効化)
|
|
16
|
+
*
|
|
17
|
+
* オンデマンド:
|
|
18
|
+
* - browse_mcp_store: Portal DBカタログ閲覧
|
|
19
|
+
* - install_from_store: Portal DBからDL+registry登録
|
|
20
|
+
* - gmail, ms365, whisper 等(activate_serviceで有効化)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
24
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
25
|
+
const {
|
|
26
|
+
CallToolRequestSchema,
|
|
27
|
+
ListToolsRequestSchema,
|
|
28
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
29
|
+
const { getPromptFromPortal, listAvailableMcps, downloadMcpPackage, getGatewayConfig, searchMcpServices, installMcpService, setServiceAlwaysOn, uninstallMcpService, updateMcpEnv, callMcpToolProxy } = require('./portal-api-client.cjs');
|
|
30
|
+
const { logger } = require('../utils/logger');
|
|
31
|
+
const { t, setLanguage } = require('../i18n');
|
|
32
|
+
const { spawn } = require('child_process');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const os = require('os');
|
|
36
|
+
|
|
37
|
+
// .envファイルを読み込み(.env.localを優先)
|
|
38
|
+
const dotenvLocalPath = path.join(__dirname, '../../.env.local');
|
|
39
|
+
const dotenvPath = path.join(__dirname, '../../.env');
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(dotenvLocalPath)) {
|
|
42
|
+
require('dotenv').config({ path: dotenvLocalPath });
|
|
43
|
+
logger.debug('[MCP Server] Loaded .env.local');
|
|
44
|
+
} else if (fs.existsSync(dotenvPath)) {
|
|
45
|
+
require('dotenv').config({ path: dotenvPath });
|
|
46
|
+
logger.debug('[MCP Server] Loaded .env');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 環境変数から取得
|
|
50
|
+
const KEYWORD = process.env.MUSUHI_KEYWORD || 'BL-要件定義';
|
|
51
|
+
const TOKEN = process.env.MUSUHI_TOKEN;
|
|
52
|
+
const isDevEnv = process.env.NODE_ENV === 'development' || (process.env.PORTAL_URL && process.env.PORTAL_URL.includes('localhost'));
|
|
53
|
+
const PORTAL_URL = process.env.PORTAL_URL || (isDevEnv ? 'http://localhost:8432' : 'https://motohi-api-235426778039.asia-northeast1.run.app');
|
|
54
|
+
const LANGUAGE = process.env.MUSUHI_LANGUAGE || 'ja';
|
|
55
|
+
|
|
56
|
+
setLanguage(LANGUAGE);
|
|
57
|
+
|
|
58
|
+
// ログファイル設定
|
|
59
|
+
const LOG_DIR = path.join(os.homedir(), '.bluelamp-vscode', 'logs');
|
|
60
|
+
const LOG_FILE = path.join(LOG_DIR, 'mcp.log');
|
|
61
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
62
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function log(message) {
|
|
66
|
+
const timestamp = new Date().toISOString();
|
|
67
|
+
const logMessage = `[${timestamp}] ${message}\n`;
|
|
68
|
+
fs.appendFileSync(LOG_FILE, logMessage);
|
|
69
|
+
logger.info(`[MCP Server] ${message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
log(`MCP Server started - Log file: ${LOG_FILE}`);
|
|
73
|
+
log(`Environment variables:`);
|
|
74
|
+
log(` KEYWORD: ${KEYWORD}`);
|
|
75
|
+
log(` TOKEN: ${TOKEN ? TOKEN.substring(0, 20) + '...' : 'NOT SET'}`);
|
|
76
|
+
log(` PORTAL_URL: ${PORTAL_URL}`);
|
|
77
|
+
log(` LANGUAGE: ${LANGUAGE}`);
|
|
78
|
+
|
|
79
|
+
// Phase 3-3: 旧 ~/.bluelamp/registry.json を削除(サーバーDB一元化、ローカル残骸を排除)
|
|
80
|
+
const LEGACY_REGISTRY = path.join(os.homedir(), '.bluelamp', 'registry.json');
|
|
81
|
+
if (fs.existsSync(LEGACY_REGISTRY)) {
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(LEGACY_REGISTRY);
|
|
84
|
+
log(`[startup] Legacy registry.json removed: ${LEGACY_REGISTRY}`);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
log(`[startup] Failed to remove legacy registry.json: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ========================================
|
|
91
|
+
// 子MCPプロセス管理
|
|
92
|
+
// ========================================
|
|
93
|
+
class ChildMcpProcess {
|
|
94
|
+
constructor(serviceId, command, args, env) {
|
|
95
|
+
this.serviceId = serviceId;
|
|
96
|
+
this.command = command;
|
|
97
|
+
// チルダ(~)をホームディレクトリに展開(cwdに依存しないように)
|
|
98
|
+
this.args = (args || []).map(a => typeof a === 'string' ? a.replace(/^~\//, os.homedir() + '/') : a);
|
|
99
|
+
this.env = { ...process.env, ...env };
|
|
100
|
+
this.process = null;
|
|
101
|
+
this.tools = [];
|
|
102
|
+
this.requestId = 0;
|
|
103
|
+
this.pending = new Map();
|
|
104
|
+
this.buffer = '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async start() {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
reject(new Error(`Timeout: "${this.serviceId}" failed to start within 30s`));
|
|
111
|
+
this.stop();
|
|
112
|
+
}, 30000);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
this.process = spawn(this.command, this.args, {
|
|
116
|
+
env: this.env,
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.process.on('error', (err) => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
reject(new Error(`Spawn error for "${this.serviceId}": ${err.message}`));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.process.on('exit', (code, signal) => {
|
|
126
|
+
log(`[ChildMcp] "${this.serviceId}" exited (code=${code}, signal=${signal})`);
|
|
127
|
+
this.pending.forEach(p => p.reject(new Error('Process exited')));
|
|
128
|
+
this.pending.clear();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.process.stderr.on('data', (data) => {
|
|
132
|
+
log(`[ChildMcp:${this.serviceId}:stderr] ${data.toString().trim()}`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.process.stdout.on('data', (data) => {
|
|
136
|
+
this.buffer += data.toString();
|
|
137
|
+
this._processBuffer();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// initialize → tools/list
|
|
141
|
+
this._sendRequest('initialize', {
|
|
142
|
+
protocolVersion: '2024-11-05',
|
|
143
|
+
capabilities: {},
|
|
144
|
+
clientInfo: { name: 'bluelamp-gateway', version: '1.0.0' },
|
|
145
|
+
}).then((initResult) => {
|
|
146
|
+
log(`[ChildMcp] "${this.serviceId}" initialized: ${JSON.stringify(initResult.serverInfo || {})}`);
|
|
147
|
+
this._sendNotification('notifications/initialized', {});
|
|
148
|
+
return this._sendRequest('tools/list', {});
|
|
149
|
+
}).then((toolsResult) => {
|
|
150
|
+
const rawTools = toolsResult.tools || [];
|
|
151
|
+
this.tools = rawTools.map(t => ({
|
|
152
|
+
...t,
|
|
153
|
+
name: `${this.serviceId}__${t.name}`,
|
|
154
|
+
_originalName: t.name,
|
|
155
|
+
}));
|
|
156
|
+
log(`[ChildMcp] "${this.serviceId}" ready: ${this.tools.length} tools`);
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
resolve(this.tools);
|
|
159
|
+
}).catch((err) => {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
reject(err);
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
reject(err);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async callTool(originalToolName, args) {
|
|
171
|
+
return await this._sendRequest('tools/call', {
|
|
172
|
+
name: originalToolName,
|
|
173
|
+
arguments: args,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stop() {
|
|
178
|
+
if (this.process) {
|
|
179
|
+
this.process.kill();
|
|
180
|
+
this.process = null;
|
|
181
|
+
}
|
|
182
|
+
this.pending.forEach(p => p.reject(new Error('Process stopped')));
|
|
183
|
+
this.pending.clear();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_sendRequest(method, params) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const id = ++this.requestId;
|
|
189
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
190
|
+
this.pending.set(id, { resolve, reject });
|
|
191
|
+
try {
|
|
192
|
+
this.process.stdin.write(msg);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
this.pending.delete(id);
|
|
195
|
+
reject(new Error(`Write error: ${err.message}`));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_sendNotification(method, params) {
|
|
201
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
202
|
+
try {
|
|
203
|
+
this.process.stdin.write(msg);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
log(`[ChildMcp] Notification write error: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_processBuffer() {
|
|
210
|
+
const lines = this.buffer.split('\n');
|
|
211
|
+
this.buffer = lines.pop();
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
const trimmed = line.trim();
|
|
214
|
+
if (!trimmed) continue;
|
|
215
|
+
try {
|
|
216
|
+
const msg = JSON.parse(trimmed);
|
|
217
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
218
|
+
const p = this.pending.get(msg.id);
|
|
219
|
+
this.pending.delete(msg.id);
|
|
220
|
+
if (msg.error) {
|
|
221
|
+
p.reject(new Error(`JSON-RPC error: ${JSON.stringify(msg.error)}`));
|
|
222
|
+
} else {
|
|
223
|
+
p.resolve(msg.result);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
log(`[ChildMcp:${this.serviceId}] Parse error: ${e.message} | line: ${trimmed.substring(0, 200)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class ChildMcpManager {
|
|
234
|
+
constructor() {
|
|
235
|
+
this.children = new Map();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async activateExternal(serviceId, service) {
|
|
239
|
+
const child = new ChildMcpProcess(
|
|
240
|
+
serviceId,
|
|
241
|
+
service.command,
|
|
242
|
+
service.args || [],
|
|
243
|
+
service.env || {},
|
|
244
|
+
);
|
|
245
|
+
const tools = await child.start();
|
|
246
|
+
this.children.set(serviceId, {
|
|
247
|
+
type: 'external',
|
|
248
|
+
process: child,
|
|
249
|
+
tools,
|
|
250
|
+
service,
|
|
251
|
+
});
|
|
252
|
+
return tools;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
deactivate(serviceId) {
|
|
256
|
+
const child = this.children.get(serviceId);
|
|
257
|
+
if (!child) return;
|
|
258
|
+
if (child.type === 'external' && child.process) {
|
|
259
|
+
child.process.stop();
|
|
260
|
+
}
|
|
261
|
+
this.children.delete(serviceId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
isActive(serviceId) {
|
|
265
|
+
return this.children.has(serviceId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getActiveTools() {
|
|
269
|
+
const tools = [];
|
|
270
|
+
for (const child of this.children.values()) {
|
|
271
|
+
tools.push(...(child.tools || []));
|
|
272
|
+
}
|
|
273
|
+
return tools;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async callTool(toolName, args) {
|
|
277
|
+
for (const [serviceId, child] of this.children) {
|
|
278
|
+
if (child.type === 'external') {
|
|
279
|
+
const tool = child.tools.find(t => t.name === toolName);
|
|
280
|
+
if (tool) {
|
|
281
|
+
const result = await child.process.callTool(tool._originalName, args);
|
|
282
|
+
return { found: true, result };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { found: false };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getActiveServiceIds() {
|
|
290
|
+
return [...this.children.keys()];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ========================================
|
|
295
|
+
// BlueLamp MCPサーバー + ゲートウェイ
|
|
296
|
+
// ========================================
|
|
297
|
+
class BlueLampServer {
|
|
298
|
+
constructor() {
|
|
299
|
+
this.childManager = new ChildMcpManager();
|
|
300
|
+
|
|
301
|
+
this.server = new Server(
|
|
302
|
+
{ name: 'bluelamp', version: '2.0.0' },
|
|
303
|
+
{
|
|
304
|
+
capabilities: {
|
|
305
|
+
tools: { listChanged: true },
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
this.setupHandlers();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
checkAuth() {
|
|
314
|
+
if (!TOKEN) {
|
|
315
|
+
return { ok: false, error: 'MUSUHI_TOKEN が設定されていません。BlueLampにログインしてください。' };
|
|
316
|
+
}
|
|
317
|
+
return { ok: true };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
buildToolList() {
|
|
321
|
+
const tools = [
|
|
322
|
+
// --- 常時ツール ---
|
|
323
|
+
{
|
|
324
|
+
name: 'load_knowledge',
|
|
325
|
+
description: t('mcp.toolDescriptionLoadKnowledge'),
|
|
326
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: {
|
|
330
|
+
keyword: { type: 'string', description: t('mcp.keywordDescription') },
|
|
331
|
+
},
|
|
332
|
+
required: ['keyword'],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'search_mcp',
|
|
337
|
+
description: 'キーワードでインストール済みMCPサービスを検索します。使いたい機能を自然言語で説明してください。(例: "ブラウザ操作", "メール送信", "画像生成")',
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
query: { type: 'string', description: '検索キーワード(自然言語OK)' },
|
|
342
|
+
},
|
|
343
|
+
required: ['query'],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'activate_service',
|
|
348
|
+
description: 'MCPサービスを有効化してツールを追加します。search_mcpで見つけたservice_idを指定してください。always_on: trueで次回以降も自動的に有効化されます。',
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: 'object',
|
|
351
|
+
properties: {
|
|
352
|
+
service_id: { type: 'string', description: 'サービスID' },
|
|
353
|
+
always_on: { type: 'boolean', description: 'trueにすると常時アクティブ(毎回自動起動)になります' },
|
|
354
|
+
},
|
|
355
|
+
required: ['service_id'],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'deactivate_service',
|
|
360
|
+
description: 'MCPサービスの常時アクティブ(alwaysOn)設定を解除します。次回起動時から自動有効化されなくなります。',
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: 'object',
|
|
363
|
+
properties: {
|
|
364
|
+
service_id: { type: 'string', description: 'サービスID' },
|
|
365
|
+
},
|
|
366
|
+
required: ['service_id'],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'restart_service',
|
|
371
|
+
description: 'MCPサービスのプロセスを再起動します。コード更新や設定変更の反映に使用します。',
|
|
372
|
+
inputSchema: {
|
|
373
|
+
type: 'object',
|
|
374
|
+
properties: {
|
|
375
|
+
service_id: { type: 'string', description: 'サービスID' },
|
|
376
|
+
},
|
|
377
|
+
required: ['service_id'],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: 'unregister_service',
|
|
382
|
+
description: 'MCPサービスをレジストリから完全に削除します。プロセスも停止します。',
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {
|
|
386
|
+
service_id: { type: 'string', description: '削除するサービスID' },
|
|
387
|
+
},
|
|
388
|
+
required: ['service_id'],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'register_service',
|
|
393
|
+
description: '新しいMCPサービスをゲートウェイに登録します。',
|
|
394
|
+
inputSchema: {
|
|
395
|
+
type: 'object',
|
|
396
|
+
properties: {
|
|
397
|
+
service_id: { type: 'string', description: 'サービスID(英数字、例: "microcms")' },
|
|
398
|
+
name: { type: 'string', description: 'サービス表示名' },
|
|
399
|
+
description: { type: 'string', description: 'サービスの説明' },
|
|
400
|
+
tags: { type: 'array', items: { type: 'string' }, description: '検索用タグ' },
|
|
401
|
+
command: { type: 'string', description: '実行コマンド("npx" or "node")' },
|
|
402
|
+
args: { type: 'array', items: { type: 'string' }, description: 'コマンド引数' },
|
|
403
|
+
env: { type: 'object', description: '環境変数' },
|
|
404
|
+
},
|
|
405
|
+
required: ['service_id', 'name', 'description', 'tags'],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'browse_mcp_store',
|
|
410
|
+
description: 'MCPストア(Portal)からインストール可能なMCP一覧を取得します。',
|
|
411
|
+
inputSchema: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
category: { type: 'string', description: 'カテゴリでフィルタ(省略可)' },
|
|
415
|
+
},
|
|
416
|
+
required: [],
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: 'install_from_store',
|
|
421
|
+
description: 'MCPストアからMCPをダウンロードしてインストールします。browse_mcp_storeで見つけたmcp_idを指定してください。',
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: {
|
|
425
|
+
mcp_id: { type: 'string', description: 'インストールするMCPのID' },
|
|
426
|
+
},
|
|
427
|
+
required: ['mcp_id'],
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: 'save_mcp_env',
|
|
432
|
+
description: 'MCPの環境変数(APIキー等)をサーバーに安全に保存します。ローカルには保存されません。次回起動時にサーバーから自動取得されます。解約時はアクセスできなくなります。',
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
service_id: { type: 'string', description: 'MCP サービスID(例: heygen, gemini-image)' },
|
|
437
|
+
env: {
|
|
438
|
+
type: 'object',
|
|
439
|
+
description: '環境変数のキーと値(例: {"HEYGEN_API_KEY": "sk_xxx"})',
|
|
440
|
+
additionalProperties: { type: 'string' },
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required: ['service_id', 'env'],
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
// アクティブなサービスのツールを追加(子プロセス系)
|
|
449
|
+
tools.push(...this.childManager.getActiveTools());
|
|
450
|
+
|
|
451
|
+
// クラウドプロキシツールを追加(子プロセス不要系)
|
|
452
|
+
if (this._cloudProxyTools) {
|
|
453
|
+
for (const [proxyName, info] of this._cloudProxyTools) {
|
|
454
|
+
tools.push({
|
|
455
|
+
name: proxyName,
|
|
456
|
+
description: info.description || `[${info.mcpId}] ${info.toolName}`,
|
|
457
|
+
inputSchema: info.inputSchema || { type: 'object', properties: {} },
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return tools;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
setupHandlers() {
|
|
466
|
+
// ツール一覧
|
|
467
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
468
|
+
const tools = this.buildToolList();
|
|
469
|
+
const active = this.childManager.getActiveServiceIds();
|
|
470
|
+
log(`[ListTools] ${tools.length} tools (active: ${active.join(', ') || 'none'})`);
|
|
471
|
+
return { tools };
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ツール実行
|
|
475
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
476
|
+
const { name, arguments: args } = request.params;
|
|
477
|
+
log(`[CallTool] ${name}`);
|
|
478
|
+
|
|
479
|
+
// --- load_knowledge ---
|
|
480
|
+
if (name === 'load_knowledge') {
|
|
481
|
+
const { keyword } = args;
|
|
482
|
+
log(`[load_knowledge] keyword: ${keyword}`);
|
|
483
|
+
try {
|
|
484
|
+
const result = await getPromptFromPortal(keyword, TOKEN, LANGUAGE);
|
|
485
|
+
if (!result || !result.success) {
|
|
486
|
+
const errorMessage = result?.error || t('mcp.knowledgeNotFound', { keyword });
|
|
487
|
+
return { content: [{ type: 'text', text: `❌ ${t('mcp.portalApiFailed', { error: errorMessage })}` }], isError: true };
|
|
488
|
+
}
|
|
489
|
+
const langInstruction = LANGUAGE !== 'ja' ? `\n\n[Note] The user's preferred language is English.` : '';
|
|
490
|
+
return {
|
|
491
|
+
content: [{
|
|
492
|
+
type: 'text',
|
|
493
|
+
text: `✅ ${t('mcp.knowledgeLoadSuccess')}\n\n${result.knowledge}${langInstruction}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${t('mcp.knowledgeApplied')}`,
|
|
494
|
+
}],
|
|
495
|
+
};
|
|
496
|
+
} catch (error) {
|
|
497
|
+
log(`[load_knowledge] Exception: ${error.message}`);
|
|
498
|
+
return { content: [{ type: 'text', text: `❌ ${t('mcp.knowledgeLoadError', { error: error.message })}` }], isError: true };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// --- search_mcp ---
|
|
503
|
+
if (name === 'search_mcp') {
|
|
504
|
+
const auth = this.checkAuth();
|
|
505
|
+
if (!auth.ok) {
|
|
506
|
+
return { content: [{ type: 'text', text: `🔒 ${auth.error}` }], isError: true };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!TOKEN) {
|
|
510
|
+
return { content: [{ type: 'text', text: `❌ 未認証です。MCPカタログの検索にはログインが必要です。` }], isError: true };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let results = [];
|
|
514
|
+
try {
|
|
515
|
+
const serverResult = await searchMcpServices(args.query, TOKEN);
|
|
516
|
+
if (!serverResult.success) {
|
|
517
|
+
return { content: [{ type: 'text', text: `❌ MCPカタログ検索に失敗: ${serverResult.error || 'Unknown'}` }], isError: true };
|
|
518
|
+
}
|
|
519
|
+
results = (serverResult.data || []).map(r => ({
|
|
520
|
+
id: r.mcp_id,
|
|
521
|
+
service: { name: r.name, description: r.description, tags: [], alwaysOn: r.alwaysOn || false },
|
|
522
|
+
isInstalled: r.isInstalled || false,
|
|
523
|
+
}));
|
|
524
|
+
} catch (err) {
|
|
525
|
+
log(`[search_mcp] Server search failed: ${err.message}`);
|
|
526
|
+
return { content: [{ type: 'text', text: `❌ MCPカタログ検索に失敗: ${err.message}` }], isError: true };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (results.length === 0) {
|
|
530
|
+
return {
|
|
531
|
+
content: [{ type: 'text', text: `🔍 「${args.query}」に一致するサービスはありません。\n\n別のキーワードで試すか、browse_mcp_storeでカタログを確認してください。` }],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let text = `🔍 「${args.query}」の検索結果(${results.length}件)\n\n`;
|
|
536
|
+
for (const r of results.slice(0, 10)) {
|
|
537
|
+
const id = r.id;
|
|
538
|
+
const svc = r.service;
|
|
539
|
+
const isActive = this.childManager.isActive(id);
|
|
540
|
+
const isAlwaysOn = svc.alwaysOn;
|
|
541
|
+
const installed = r.isInstalled ? '' : ' [未インストール]';
|
|
542
|
+
const status = isAlwaysOn ? '🔒' : isActive ? '🟢' : '⚪';
|
|
543
|
+
const label = isAlwaysOn ? ' [常時アクティブ]' : '';
|
|
544
|
+
text += `${status} **${svc.name}**${label}${installed} (id: \`${id}\`)\n`;
|
|
545
|
+
text += ` ${svc.description}\n\n`;
|
|
546
|
+
}
|
|
547
|
+
text += `有効化: activate_service | インストール: install_from_store`;
|
|
548
|
+
return { content: [{ type: 'text', text }] };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// --- activate_service ---
|
|
552
|
+
if (name === 'activate_service') {
|
|
553
|
+
const auth = this.checkAuth();
|
|
554
|
+
if (!auth.ok) {
|
|
555
|
+
return { content: [{ type: 'text', text: `🔒 ${auth.error}` }], isError: true };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const { service_id, always_on } = args;
|
|
559
|
+
|
|
560
|
+
// サーバーから service 設定を取得
|
|
561
|
+
const cfg = await getGatewayConfig(TOKEN);
|
|
562
|
+
if (!cfg.success) {
|
|
563
|
+
return { content: [{ type: 'text', text: `❌ サーバー設定の取得に失敗: ${cfg.error || 'Unknown'}` }], isError: true };
|
|
564
|
+
}
|
|
565
|
+
const svc = (cfg.data?.services || []).find(s => s.mcpId === service_id);
|
|
566
|
+
|
|
567
|
+
if (!svc) {
|
|
568
|
+
return { content: [{ type: 'text', text: `❌ サービス "${service_id}" はインストールされていません。install_from_storeでインストールしてください。` }], isError: true };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (always_on) {
|
|
572
|
+
setServiceAlwaysOn(service_id, true, TOKEN).catch(err =>
|
|
573
|
+
log(`[activate] Server sync failed: ${err.message}`)
|
|
574
|
+
);
|
|
575
|
+
log(`[activate] "${service_id}" set alwaysOn: true`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (this.childManager.isActive(service_id)) {
|
|
579
|
+
const alwaysOnMsg = always_on ? '(常時アクティブに設定しました)' : '';
|
|
580
|
+
return { content: [{ type: 'text', text: `ℹ️ "${service_id}" は既に有効です。${alwaysOnMsg}` }] };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// cloud_proxy → 子プロセスを起動せず、プロキシツールとして直接登録
|
|
584
|
+
//(autoActivate と同じロジック)
|
|
585
|
+
if (svc.hostingType === 'cloud_proxy') {
|
|
586
|
+
const proxyTools = Array.isArray(svc.tools) ? svc.tools : [];
|
|
587
|
+
if (proxyTools.length === 0) {
|
|
588
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" (cloud_proxy) にツール定義がありません。` }], isError: true };
|
|
589
|
+
}
|
|
590
|
+
// 既に同名で登録済みの場合は上書き(再アクティブで安全)
|
|
591
|
+
this._cloudProxyTools = this._cloudProxyTools || new Map();
|
|
592
|
+
for (const tool of proxyTools) {
|
|
593
|
+
const proxyToolName = `${service_id}__${tool.name}`;
|
|
594
|
+
this._cloudProxyTools.set(proxyToolName, {
|
|
595
|
+
mcpId: service_id,
|
|
596
|
+
toolName: tool.name,
|
|
597
|
+
description: tool.description,
|
|
598
|
+
inputSchema: tool.inputSchema,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
log(`[activate] "${service_id}" (cloud_proxy) → ${proxyTools.length} proxy tools registered`);
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
605
|
+
} catch (err) {
|
|
606
|
+
log(`[activate] notification error: ${err.message}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const toolNames = proxyTools.map(t => `${service_id}__${t.name}`).join(', ');
|
|
610
|
+
const alwaysOnLabel = always_on ? ' 🔒常時アクティブ' : '';
|
|
611
|
+
return {
|
|
612
|
+
content: [{ type: 'text', text: `✅ "${svc.name}" を有効化しました${alwaysOnLabel}(cloud_proxy ツール${proxyTools.length}個追加)\n\n追加ツール: ${toolNames}` }],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// external_api / local → 子プロセス spawn
|
|
617
|
+
const installDir = path.join(os.homedir(), '.claude', 'mcp-servers', service_id);
|
|
618
|
+
const entryPoint = svc.entryPoint || 'dist/index.js';
|
|
619
|
+
const service = {
|
|
620
|
+
name: svc.name,
|
|
621
|
+
description: svc.description,
|
|
622
|
+
type: 'external',
|
|
623
|
+
command: svc.command || 'node',
|
|
624
|
+
args: svc.args && svc.args.length ? svc.args : [path.join(installDir, entryPoint)],
|
|
625
|
+
env: svc.env || {},
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
let addedTools;
|
|
629
|
+
try {
|
|
630
|
+
addedTools = await this.childManager.activateExternal(service_id, service);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
log(`[activate] Failed "${service_id}": ${err.message}`);
|
|
633
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" の起動に失敗しました: ${err.message}` }], isError: true };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const toolNames = addedTools.map(t => t.name).join(', ');
|
|
637
|
+
const alwaysOnLabel = always_on ? ' 🔒常時アクティブ' : '';
|
|
638
|
+
log(`[activate] "${service_id}" → ${addedTools.length} tools${always_on ? ' (alwaysOn)' : ''}. Sending list_changed.`);
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
642
|
+
} catch (err) {
|
|
643
|
+
log(`[activate] notification error: ${err.message}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
content: [{ type: 'text', text: `✅ "${service.name}" を有効化しました${alwaysOnLabel}(ツール${addedTools.length}個追加)\n\n追加ツール: ${toolNames || '(なし)'}` }],
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// --- deactivate_service ---
|
|
652
|
+
if (name === 'deactivate_service') {
|
|
653
|
+
const { service_id } = args;
|
|
654
|
+
|
|
655
|
+
if (!TOKEN) {
|
|
656
|
+
return { content: [{ type: 'text', text: `🔒 未認証です。ログインが必要です。` }], isError: true };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const result = await setServiceAlwaysOn(service_id, false, TOKEN);
|
|
660
|
+
if (!result.success) {
|
|
661
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" の解除に失敗: ${result.error || 'Unknown'}` }], isError: true };
|
|
662
|
+
}
|
|
663
|
+
log(`[deactivate] "${service_id}" set alwaysOn: false`);
|
|
664
|
+
|
|
665
|
+
// cloud_proxy: 登録済みプロキシツールを Map から削除 → list_changed 通知
|
|
666
|
+
let removedCount = 0;
|
|
667
|
+
if (this._cloudProxyTools) {
|
|
668
|
+
const prefix = `${service_id}__`;
|
|
669
|
+
for (const key of [...this._cloudProxyTools.keys()]) {
|
|
670
|
+
if (key.startsWith(prefix)) {
|
|
671
|
+
this._cloudProxyTools.delete(key);
|
|
672
|
+
removedCount++;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (removedCount > 0) {
|
|
676
|
+
log(`[deactivate] "${service_id}" (cloud_proxy) → ${removedCount} proxy tools removed`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// external_api / local: 子プロセスが動いていれば停止
|
|
681
|
+
let stoppedChild = false;
|
|
682
|
+
if (this.childManager.isActive(service_id)) {
|
|
683
|
+
this.childManager.deactivate(service_id);
|
|
684
|
+
log(`[deactivate] "${service_id}" child process stopped`);
|
|
685
|
+
stoppedChild = true;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ツール一覧が変化した場合のみ list_changed を送る
|
|
689
|
+
if (removedCount > 0 || stoppedChild) {
|
|
690
|
+
try {
|
|
691
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
692
|
+
} catch (err) {
|
|
693
|
+
log(`[deactivate] notification error: ${err.message}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let text = `✅ "${service_id}" の常時アクティブを解除しました。`;
|
|
698
|
+
if (removedCount > 0) text += ` cloud_proxy ツール${removedCount}個を削除しました。`;
|
|
699
|
+
if (stoppedChild) text += ` 子プロセスを停止しました。`;
|
|
700
|
+
text += ` 次回起動時から自動有効化されません。`;
|
|
701
|
+
return { content: [{ type: 'text', text }] };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// --- restart_service ---
|
|
705
|
+
if (name === 'restart_service') {
|
|
706
|
+
const { service_id } = args;
|
|
707
|
+
|
|
708
|
+
// アクティブな子プロセスから設定を取得(activateExternal 時に保存済み)
|
|
709
|
+
const child = this.childManager.children.get(service_id);
|
|
710
|
+
const service = child?.service;
|
|
711
|
+
|
|
712
|
+
if (!service) {
|
|
713
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" は現在アクティブではありません。activate_service で有効化してください。` }], isError: true };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 1. 既存プロセスを停止
|
|
717
|
+
this.childManager.deactivate(service_id);
|
|
718
|
+
log(`[restart] "${service_id}" stopped`);
|
|
719
|
+
|
|
720
|
+
// 2. 再起動
|
|
721
|
+
let addedTools;
|
|
722
|
+
try {
|
|
723
|
+
addedTools = await this.childManager.activateExternal(service_id, service);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
log(`[restart] Failed "${service_id}": ${err.message}`);
|
|
726
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" の再起動に失敗しました: ${err.message}` }], isError: true };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
log(`[restart] "${service_id}" → ${addedTools.length} tools. Sending list_changed.`);
|
|
730
|
+
try {
|
|
731
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
732
|
+
} catch (err) {
|
|
733
|
+
log(`[restart] notification error: ${err.message}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const toolNames = addedTools.map(t => t.name).join(', ');
|
|
737
|
+
return {
|
|
738
|
+
content: [{ type: 'text', text: `🔄 "${service.name}" を再起動しました(ツール${addedTools.length}個)\n\nツール: ${toolNames || '(なし)'}` }],
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// --- unregister_service ---
|
|
743
|
+
if (name === 'unregister_service') {
|
|
744
|
+
const { service_id } = args;
|
|
745
|
+
|
|
746
|
+
if (!TOKEN) {
|
|
747
|
+
return { content: [{ type: 'text', text: `🔒 未認証です。ログインが必要です。` }], isError: true };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 1. プロセス停止
|
|
751
|
+
if (this.childManager.isActive(service_id)) {
|
|
752
|
+
this.childManager.deactivate(service_id);
|
|
753
|
+
log(`[unregister] "${service_id}" stopped`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 2. サーバーからアンインストール
|
|
757
|
+
const result = await uninstallMcpService(service_id, TOKEN);
|
|
758
|
+
if (!result.success) {
|
|
759
|
+
return { content: [{ type: 'text', text: `❌ "${service_id}" のサーバー削除に失敗: ${result.error || 'Unknown'}` }], isError: true };
|
|
760
|
+
}
|
|
761
|
+
log(`[unregister] "${service_id}" uninstalled from server`);
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
765
|
+
} catch (err) {
|
|
766
|
+
log(`[unregister] notification error: ${err.message}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
content: [{ type: 'text', text: `🗑️ "${service_id}" を削除しました。プロセスも停止済みです。` }],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- register_service ---
|
|
775
|
+
if (name === 'register_service') {
|
|
776
|
+
const auth = this.checkAuth();
|
|
777
|
+
if (!auth.ok) {
|
|
778
|
+
return { content: [{ type: 'text', text: `🔒 ${auth.error}` }], isError: true };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const { service_id, env } = args;
|
|
782
|
+
|
|
783
|
+
if (!TOKEN) {
|
|
784
|
+
return { content: [{ type: 'text', text: `🔒 未認証です。ログインが必要です。` }], isError: true };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const envObj = env || {};
|
|
788
|
+
const installResult = await installMcpService(service_id, false, TOKEN, envObj);
|
|
789
|
+
|
|
790
|
+
if (!installResult.success) {
|
|
791
|
+
return {
|
|
792
|
+
content: [{ type: 'text', text: `❌ "${service_id}" の登録に失敗しました: ${installResult.error || 'Unknown'}\n\n※ MCP はサーバーカタログに登録されている必要があります。browse_mcp_store で確認してください。` }],
|
|
793
|
+
isError: true,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
log(`[register] "${service_id}" installed on server`);
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
content: [{ type: 'text', text: `✅ サービス "${service_id}" をサーバーに登録しました。\n\n次のステップ:\n- save_mcp_env で APIキー等を設定\n- activate_service で有効化` }],
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// --- save_mcp_env ---
|
|
805
|
+
if (name === 'save_mcp_env') {
|
|
806
|
+
const auth = this.checkAuth();
|
|
807
|
+
if (!auth.ok) {
|
|
808
|
+
return { content: [{ type: 'text', text: `🔒 ${auth.error}` }], isError: true };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const { service_id, env } = args;
|
|
812
|
+
if (!service_id || !env || Object.keys(env).length === 0) {
|
|
813
|
+
return { content: [{ type: 'text', text: '❌ service_id と env(キー/値のペア)が必要です' }], isError: true };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
log(`[save_mcp_env] ${service_id}: ${Object.keys(env).join(', ')}`);
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
const result = await updateMcpEnv(service_id, env, TOKEN);
|
|
820
|
+
if (!result.success) {
|
|
821
|
+
return { content: [{ type: 'text', text: `❌ サーバー保存失敗: ${result.error}` }], isError: true };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const keys = Object.keys(env).join(', ');
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: 'text', text: `🔐 環境変数をサーバーに保存しました\n\nサービス: ${service_id}\n保存キー: ${keys}\n\n※ ローカルには保存されません。次回起動時にサーバーから自動取得されます。` }],
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
log(`[save_mcp_env] error: ${error.message}`);
|
|
830
|
+
return { content: [{ type: 'text', text: `❌ ${error.message}` }], isError: true };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// --- browse_mcp_store (旧 list_mcps) ---
|
|
835
|
+
if (name === 'browse_mcp_store') {
|
|
836
|
+
const { category } = args || {};
|
|
837
|
+
log(`[browse_mcp_store] category=${category || 'all'}`);
|
|
838
|
+
try {
|
|
839
|
+
const options = category ? { category } : {};
|
|
840
|
+
const result = await listAvailableMcps(options, TOKEN, LANGUAGE);
|
|
841
|
+
if (!result || !result.success) {
|
|
842
|
+
return { content: [{ type: 'text', text: `❌ MCPカタログの取得に失敗: ${result?.error || 'Unknown'}` }], isError: true };
|
|
843
|
+
}
|
|
844
|
+
const mcps = result.data || [];
|
|
845
|
+
if (mcps.length === 0) {
|
|
846
|
+
return { content: [{ type: 'text', text: '📦 インストール可能なMCPはありません。' }] };
|
|
847
|
+
}
|
|
848
|
+
let text = `📦 MCPストア一覧 (${mcps.length}件)\n\n`;
|
|
849
|
+
mcps.forEach(mcp => {
|
|
850
|
+
const access = mcp.hasAccess ? '✅' : '🔒';
|
|
851
|
+
const official = mcp.isOfficial ? ' [公式]' : '';
|
|
852
|
+
text += `${access} **${mcp.name}**${official}\n`;
|
|
853
|
+
text += ` ID: ${mcp.mcpId}\n`;
|
|
854
|
+
text += ` ${mcp.description}\n`;
|
|
855
|
+
text += ` カテゴリ: ${mcp.category} | ツール数: ${mcp.toolCount}\n\n`;
|
|
856
|
+
});
|
|
857
|
+
text += `\nインストールするには: install_from_store(mcp_id: "MCP_ID")`;
|
|
858
|
+
return { content: [{ type: 'text', text }] };
|
|
859
|
+
} catch (error) {
|
|
860
|
+
log(`[browse_mcp_store] error: ${error.message}`);
|
|
861
|
+
return { content: [{ type: 'text', text: `❌ ${error.message}` }], isError: true };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// --- install_from_store (旧 install_mcp) ---
|
|
866
|
+
if (name === 'install_from_store') {
|
|
867
|
+
const { mcp_id } = args;
|
|
868
|
+
log(`[install_from_store] mcp_id=${mcp_id}`);
|
|
869
|
+
try {
|
|
870
|
+
const result = await downloadMcpPackage(mcp_id, TOKEN);
|
|
871
|
+
if (!result || !result.success) {
|
|
872
|
+
return { content: [{ type: 'text', text: `❌ MCPダウンロードに失敗: ${result?.error || 'Unknown'}` }], isError: true };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const pkg = result.data;
|
|
876
|
+
const pkgSource = pkg.packageSource || {};
|
|
877
|
+
const files = pkg.files || [];
|
|
878
|
+
|
|
879
|
+
// cloud_proxy 判定: packageSource.hostingType(バックエンド v2+)を優先。
|
|
880
|
+
// 未提供の旧バックエンドではサーバー登録後に getGatewayConfig で判定する。
|
|
881
|
+
let hostingType = pkgSource.hostingType || (pkgSource.hosting && pkgSource.hosting.type) || null;
|
|
882
|
+
|
|
883
|
+
// サーバーにインストール登録(ユーザーの UserMcpInstallation に追加)
|
|
884
|
+
// cloud_proxy 判定のために先に登録しておく(hostingType 取得目的も兼ねる)
|
|
885
|
+
if (TOKEN) {
|
|
886
|
+
const installResult = await installMcpService(mcp_id, false, TOKEN);
|
|
887
|
+
if (!installResult.success) {
|
|
888
|
+
return { content: [{ type: 'text', text: `❌ サーバー登録失敗: ${installResult.error || 'Unknown'}` }], isError: true };
|
|
889
|
+
}
|
|
890
|
+
log(`[install_from_store] Server sync OK: ${mcp_id}`);
|
|
891
|
+
|
|
892
|
+
// hostingType が packageSource に無かった場合、gateway-config から取得
|
|
893
|
+
if (!hostingType) {
|
|
894
|
+
const cfg = await getGatewayConfig(TOKEN);
|
|
895
|
+
if (cfg.success) {
|
|
896
|
+
const svc = (cfg.data?.services || []).find(s => s.mcpId === mcp_id);
|
|
897
|
+
if (svc) hostingType = svc.hostingType;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// cloud_proxy は子プロセス不要 → ローカル展開スキップ(ゲートウェイがプロキシ転送)
|
|
903
|
+
if (hostingType === 'cloud_proxy') {
|
|
904
|
+
log(`[install_from_store] "${mcp_id}" is cloud_proxy, skipping local extract`);
|
|
905
|
+
let text = `✅ MCP「${pkgSource.name || mcp_id}」をインストールしました(cloud_proxy)\n\n`;
|
|
906
|
+
text += `☁ クラウド実行のためローカル展開は不要です\n`;
|
|
907
|
+
if (pkgSource.installNotes) text += `\n📝 ${pkgSource.installNotes}\n`;
|
|
908
|
+
text += `\nactivate_service(service_id: "${mcp_id}") で有効化できます。`;
|
|
909
|
+
return { content: [{ type: 'text', text }] };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// external_api / local: ファイル展開が必要
|
|
913
|
+
if (files.length === 0) {
|
|
914
|
+
return { content: [{ type: 'text', text: '❌ MCPパッケージにファイルが含まれていません。' }], isError: true };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ファイル展開
|
|
918
|
+
const installDir = path.join(os.homedir(), '.claude', 'mcp-servers', mcp_id);
|
|
919
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
920
|
+
for (const file of files) {
|
|
921
|
+
const filePath = path.join(installDir, file.path);
|
|
922
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
923
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
924
|
+
if (typeof file.mode === 'number') {
|
|
925
|
+
try { fs.chmodSync(filePath, file.mode & 0o777); } catch (_) { /* swallow */ }
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
log(`[install_from_store] ${files.length} files written to ${installDir}`);
|
|
929
|
+
|
|
930
|
+
// npm install
|
|
931
|
+
if (pkgSource.needsNpmInstall && fs.existsSync(path.join(installDir, 'package.json'))) {
|
|
932
|
+
const { execSync } = require('child_process');
|
|
933
|
+
try {
|
|
934
|
+
execSync('npm install --production', { cwd: installDir, timeout: 60000, stdio: 'pipe' });
|
|
935
|
+
} catch (npmError) {
|
|
936
|
+
return { content: [{ type: 'text', text: `⚠️ ファイル展開完了、npm install失敗: ${npmError.message}` }] };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let text = `✅ MCP「${pkgSource.name || mcp_id}」をインストールしました\n\n`;
|
|
941
|
+
text += `📁 インストール先: ${installDir}\n`;
|
|
942
|
+
text += `📄 ファイル数: ${files.length}\n`;
|
|
943
|
+
if (pkgSource.installNotes) text += `\n📝 ${pkgSource.installNotes}\n`;
|
|
944
|
+
const requiredEnvVars = pkgSource.requiredEnvVars || [];
|
|
945
|
+
const missingVars = requiredEnvVars.filter(v => !process.env[v]);
|
|
946
|
+
if (missingVars.length > 0) {
|
|
947
|
+
text += `\n⚠️ 以下の環境変数を save_mcp_env で設定してください:\n`;
|
|
948
|
+
missingVars.forEach(v => { text += ` - ${v}\n`; });
|
|
949
|
+
}
|
|
950
|
+
text += `\nactivate_service(service_id: "${mcp_id}") で有効化できます。`;
|
|
951
|
+
return { content: [{ type: 'text', text }] };
|
|
952
|
+
} catch (error) {
|
|
953
|
+
log(`[install_from_store] error: ${error.message}`);
|
|
954
|
+
return { content: [{ type: 'text', text: `❌ ${error.message}` }], isError: true };
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// --- クラウドプロキシツール(子プロセス不要、直接サーバーに転送)---
|
|
959
|
+
if (this._cloudProxyTools && this._cloudProxyTools.has(name)) {
|
|
960
|
+
const proxyInfo = this._cloudProxyTools.get(name);
|
|
961
|
+
log(`[callTool] Cloud proxy: ${name} → ${proxyInfo.mcpId}/${proxyInfo.toolName}`);
|
|
962
|
+
try {
|
|
963
|
+
const result = await callMcpToolProxy(proxyInfo.mcpId, proxyInfo.toolName, args, TOKEN);
|
|
964
|
+
if (result.success && result.data) {
|
|
965
|
+
// callMcpTool のレスポンスは JSON-RPC 形式の可能性がある
|
|
966
|
+
if (result.data.content) return result.data;
|
|
967
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
|
|
968
|
+
}
|
|
969
|
+
return { content: [{ type: 'text', text: `❌ プロキシエラー: ${result.error || 'Unknown'}` }], isError: true };
|
|
970
|
+
} catch (err) {
|
|
971
|
+
log(`[callTool] Cloud proxy error "${name}": ${err.message}`);
|
|
972
|
+
return { content: [{ type: 'text', text: `❌ クラウドプロキシエラー: ${err.message}` }], isError: true };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// --- 動的ツール(アクティブサービスから検索 = 子プロセス経由)---
|
|
977
|
+
try {
|
|
978
|
+
const { found, result } = await this.childManager.callTool(name, args);
|
|
979
|
+
if (found) {
|
|
980
|
+
if (result && result.content) return result;
|
|
981
|
+
return { content: [{ type: 'text', text: String(result) }] };
|
|
982
|
+
}
|
|
983
|
+
} catch (err) {
|
|
984
|
+
log(`[callTool] Error proxying "${name}": ${err.message}`);
|
|
985
|
+
return { content: [{ type: 'text', text: `❌ ツール実行エラー: ${err.message}` }], isError: true };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return { content: [{ type: 'text', text: `❌ Unknown tool: ${name}` }], isError: true };
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async autoActivate() {
|
|
993
|
+
// Phase 3-3: サーバーを唯一の真実源として使用
|
|
994
|
+
// cloud_proxy → プロキシツールとして直接登録(子プロセス不要)
|
|
995
|
+
// external_api / local → 子プロセス spawn
|
|
996
|
+
if (!TOKEN) {
|
|
997
|
+
log(`[autoActivate] No token, skipping auto-activation`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
let serverServices = [];
|
|
1002
|
+
try {
|
|
1003
|
+
const result = await getGatewayConfig(TOKEN);
|
|
1004
|
+
if (!result.success) {
|
|
1005
|
+
log(`[autoActivate] Server config failed: ${result.error || 'Unknown'}. Starting with no services.`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
serverServices = result.data?.services || [];
|
|
1009
|
+
log(`[autoActivate] Server config: ${serverServices.length} services`);
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
log(`[autoActivate] Server config error: ${err.message}. Starting with no services.`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
for (const svc of serverServices) {
|
|
1016
|
+
if (!svc.alwaysOn) continue;
|
|
1017
|
+
|
|
1018
|
+
if (svc.hostingType === 'cloud_proxy') {
|
|
1019
|
+
const tools = Array.isArray(svc.tools) ? svc.tools : [];
|
|
1020
|
+
if (tools.length === 0) {
|
|
1021
|
+
log(`[autoActivate] "${svc.mcpId}" (cloud_proxy) has no tools, skipping`);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
for (const tool of tools) {
|
|
1025
|
+
const proxyToolName = `${svc.mcpId}__${tool.name}`;
|
|
1026
|
+
this._cloudProxyTools = this._cloudProxyTools || new Map();
|
|
1027
|
+
this._cloudProxyTools.set(proxyToolName, {
|
|
1028
|
+
mcpId: svc.mcpId,
|
|
1029
|
+
toolName: tool.name,
|
|
1030
|
+
description: tool.description,
|
|
1031
|
+
inputSchema: tool.inputSchema,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
log(`[autoActivate] "${svc.mcpId}" (cloud_proxy) → ${tools.length} proxy tools registered`);
|
|
1035
|
+
} else {
|
|
1036
|
+
const installDir = path.join(os.homedir(), '.claude', 'mcp-servers', svc.mcpId);
|
|
1037
|
+
const entryPoint = svc.entryPoint || 'dist/index.js';
|
|
1038
|
+
const service = {
|
|
1039
|
+
name: svc.name,
|
|
1040
|
+
description: svc.description,
|
|
1041
|
+
type: 'external',
|
|
1042
|
+
command: svc.command || 'node',
|
|
1043
|
+
args: svc.args && svc.args.length ? svc.args : [path.join(installDir, entryPoint)],
|
|
1044
|
+
env: svc.env || {},
|
|
1045
|
+
};
|
|
1046
|
+
log(`[autoActivate] Activating "${svc.mcpId}" (${svc.hostingType})...`);
|
|
1047
|
+
try {
|
|
1048
|
+
const tools = await this.childManager.activateExternal(svc.mcpId, service);
|
|
1049
|
+
log(`[autoActivate] "${svc.mcpId}" ready: ${tools.length} tools`);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
log(`[autoActivate] "${svc.mcpId}" failed: ${err.message}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async run() {
|
|
1058
|
+
const transport = new StdioServerTransport();
|
|
1059
|
+
await this.server.connect(transport);
|
|
1060
|
+
log('[Gateway] BlueLamp Gateway v2.0.0 started.');
|
|
1061
|
+
|
|
1062
|
+
// autoActivate処理
|
|
1063
|
+
await this.autoActivate();
|
|
1064
|
+
|
|
1065
|
+
// tools/list_changed を送信(autoActivateで追加されたツールを反映)
|
|
1066
|
+
if (this.childManager.getActiveServiceIds().length > 0) {
|
|
1067
|
+
try {
|
|
1068
|
+
await this.server.notification({ method: 'notifications/tools/list_changed' });
|
|
1069
|
+
log('[Gateway] Initial list_changed sent after autoActivate.');
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
log(`[Gateway] Initial list_changed error: ${err.message}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 起動時に自動的に知識を読み込み
|
|
1076
|
+
if (KEYWORD && TOKEN) {
|
|
1077
|
+
log(`[Gateway] Auto-loading knowledge for keyword: ${KEYWORD}`);
|
|
1078
|
+
const result = await getPromptFromPortal(KEYWORD, TOKEN, LANGUAGE);
|
|
1079
|
+
if (result && result.success) {
|
|
1080
|
+
log(`[Gateway] Auto-load successful: ${result.title}`);
|
|
1081
|
+
} else {
|
|
1082
|
+
log(`[Gateway] Auto-load failed for keyword: ${KEYWORD}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const server = new BlueLampServer();
|
|
1089
|
+
|
|
1090
|
+
// === Zombie prevention: cleanup all child MCPs on parent exit ===
|
|
1091
|
+
let _blIsShuttingDown = false;
|
|
1092
|
+
const _blCleanupChildren = (signal) => {
|
|
1093
|
+
if (_blIsShuttingDown) return;
|
|
1094
|
+
_blIsShuttingDown = true;
|
|
1095
|
+
try { log(`[Gateway] Shutdown (${signal}) - cleaning up children...`); } catch (e) {}
|
|
1096
|
+
if (server && server.childManager && server.childManager.children) {
|
|
1097
|
+
for (const [id, child] of server.childManager.children) {
|
|
1098
|
+
try {
|
|
1099
|
+
try { log(`[Gateway] Stopping child: ${id}`); } catch (e) {}
|
|
1100
|
+
if (child && child.type === 'external' && child.process && typeof child.process.stop === 'function') {
|
|
1101
|
+
child.process.stop();
|
|
1102
|
+
}
|
|
1103
|
+
} catch (e) {}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
setTimeout(() => process.exit(0), 200).unref();
|
|
1107
|
+
};
|
|
1108
|
+
process.on('SIGTERM', () => _blCleanupChildren('SIGTERM'));
|
|
1109
|
+
process.on('SIGINT', () => _blCleanupChildren('SIGINT'));
|
|
1110
|
+
process.on('SIGHUP', () => _blCleanupChildren('SIGHUP'));
|
|
1111
|
+
process.stdin.on('end', () => _blCleanupChildren('stdin-end'));
|
|
1112
|
+
process.stdin.on('close', () => _blCleanupChildren('stdin-close'));
|
|
1113
|
+
process.on('exit', () => {
|
|
1114
|
+
if (server && server.childManager && server.childManager.children) {
|
|
1115
|
+
for (const [id, child] of server.childManager.children) {
|
|
1116
|
+
try {
|
|
1117
|
+
if (child && child.process && child.process.process && typeof child.process.process.kill === 'function') {
|
|
1118
|
+
child.process.process.kill('SIGKILL');
|
|
1119
|
+
}
|
|
1120
|
+
} catch (e) {}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
// =============================================================
|
|
1125
|
+
|
|
1126
|
+
server.run().catch((err) => {
|
|
1127
|
+
log(`[Gateway] Fatal: ${err.message}`);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
});
|