bluelamp 4.2.2 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bluelamp-vscode-base +1 -1
- package/lib/auth/InteractiveLogin.js +1 -1
- package/lib/auth/PortalAuthClient.js +1 -1
- package/lib/auth/TokenManager.js +1 -1
- package/lib/mcp-gmail/index.js +141 -1
- package/lib/mcp-server/index.cjs +1 -1
- package/lib/mcp-server/portal-api-client.cjs +1 -1
- package/package.json +1 -1
- package/scripts/playwright-launch.js +1 -1
package/lib/mcp-gmail/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');
|
|
|
20
20
|
const ACCOUNTS_PATH = path.join(CONFIG_DIR, 'accounts.json');
|
|
21
21
|
// Legacy single-token path (for migration)
|
|
22
22
|
const LEGACY_TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
|
|
23
|
-
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
|
|
23
|
+
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.compose'];
|
|
24
24
|
const DEFAULT_TEXT_BODY_LIMIT = 10000;
|
|
25
25
|
// --- Account discovery ---
|
|
26
26
|
function getAccounts() {
|
|
@@ -293,6 +293,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
293
293
|
required: ['items'],
|
|
294
294
|
},
|
|
295
295
|
},
|
|
296
|
+
{
|
|
297
|
+
name: 'prepare_outbox',
|
|
298
|
+
description: 'Stage a new message for review with optional file attachments. Saved to outbox for user approval before delivery.',
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
account: { type: 'string', description: 'Sender email account' },
|
|
303
|
+
to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses' },
|
|
304
|
+
cc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses (optional)' },
|
|
305
|
+
bcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses (optional)' },
|
|
306
|
+
subject: { type: 'string', description: 'Email subject' },
|
|
307
|
+
body: { type: 'string', description: 'Email body (plain text)' },
|
|
308
|
+
html_body: { type: 'string', description: 'Email body (HTML, optional - overrides body if set)' },
|
|
309
|
+
attachments: {
|
|
310
|
+
type: 'array',
|
|
311
|
+
items: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
file_path: { type: 'string', description: 'Absolute path to the file to attach' },
|
|
315
|
+
filename: { type: 'string', description: 'Override filename (optional, defaults to basename)' },
|
|
316
|
+
},
|
|
317
|
+
required: ['file_path'],
|
|
318
|
+
},
|
|
319
|
+
description: 'File attachments (optional)',
|
|
320
|
+
},
|
|
321
|
+
reply_to_message_id: { type: 'string', description: 'Gmail message ID to reply to (optional, sets In-Reply-To and References headers)' },
|
|
322
|
+
},
|
|
323
|
+
required: ['account', 'to', 'subject', 'body'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
296
326
|
],
|
|
297
327
|
}));
|
|
298
328
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -505,6 +535,116 @@ ${htmlBody}
|
|
|
505
535
|
}
|
|
506
536
|
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
507
537
|
}
|
|
538
|
+
case 'prepare_outbox': {
|
|
539
|
+
const { account, to, cc, bcc, subject, body, html_body, attachments, reply_to_message_id } = args;
|
|
540
|
+
const auth = getAuthenticatedClient(account);
|
|
541
|
+
if (!auth) {
|
|
542
|
+
return {
|
|
543
|
+
content: [{ type: 'text', text: `Account ${account} not authenticated. Use authorize_account first.` }],
|
|
544
|
+
isError: true,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const gmail = getGmail(auth);
|
|
548
|
+
const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
549
|
+
const mimeLines = [];
|
|
550
|
+
mimeLines.push(`To: ${to.join(', ')}`);
|
|
551
|
+
if (cc?.length) mimeLines.push(`Cc: ${cc.join(', ')}`);
|
|
552
|
+
if (bcc?.length) mimeLines.push(`Bcc: ${bcc.join(', ')}`);
|
|
553
|
+
mimeLines.push(`Subject: =?UTF-8?B?${Buffer.from(subject).toString('base64')}?=`);
|
|
554
|
+
mimeLines.push('MIME-Version: 1.0');
|
|
555
|
+
if (reply_to_message_id) {
|
|
556
|
+
try {
|
|
557
|
+
const origMsg = await gmail.users.messages.get({ userId: 'me', id: reply_to_message_id, format: 'metadata', metadataHeaders: ['Message-ID'] });
|
|
558
|
+
const origMsgId = getHeader(origMsg.data.payload?.headers || [], 'Message-ID');
|
|
559
|
+
if (origMsgId) {
|
|
560
|
+
mimeLines.push(`In-Reply-To: ${origMsgId}`);
|
|
561
|
+
mimeLines.push(`References: ${origMsgId}`);
|
|
562
|
+
}
|
|
563
|
+
} catch {}
|
|
564
|
+
}
|
|
565
|
+
const hasAttachments = attachments?.length > 0;
|
|
566
|
+
if (hasAttachments) {
|
|
567
|
+
mimeLines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
568
|
+
mimeLines.push('');
|
|
569
|
+
mimeLines.push(`--${boundary}`);
|
|
570
|
+
}
|
|
571
|
+
if (html_body) {
|
|
572
|
+
mimeLines.push('Content-Type: text/html; charset=UTF-8');
|
|
573
|
+
mimeLines.push('Content-Transfer-Encoding: base64');
|
|
574
|
+
mimeLines.push('');
|
|
575
|
+
mimeLines.push(Buffer.from(html_body).toString('base64'));
|
|
576
|
+
} else {
|
|
577
|
+
mimeLines.push('Content-Type: text/plain; charset=UTF-8');
|
|
578
|
+
mimeLines.push('Content-Transfer-Encoding: base64');
|
|
579
|
+
mimeLines.push('');
|
|
580
|
+
mimeLines.push(Buffer.from(body).toString('base64'));
|
|
581
|
+
}
|
|
582
|
+
if (hasAttachments) {
|
|
583
|
+
for (const att of attachments) {
|
|
584
|
+
const filePath = att.file_path;
|
|
585
|
+
if (!fs.existsSync(filePath)) {
|
|
586
|
+
return { content: [{ type: 'text', text: `Attachment file not found: ${filePath}` }], isError: true };
|
|
587
|
+
}
|
|
588
|
+
const fileData = fs.readFileSync(filePath);
|
|
589
|
+
const fileName = att.filename || path.basename(filePath);
|
|
590
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
591
|
+
const mimeTypes = {
|
|
592
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
593
|
+
'.xls': 'application/vnd.ms-excel',
|
|
594
|
+
'.pdf': 'application/pdf',
|
|
595
|
+
'.csv': 'text/csv',
|
|
596
|
+
'.doc': 'application/msword',
|
|
597
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
598
|
+
'.zip': 'application/zip',
|
|
599
|
+
'.png': 'image/png',
|
|
600
|
+
'.jpg': 'image/jpeg',
|
|
601
|
+
'.jpeg': 'image/jpeg',
|
|
602
|
+
};
|
|
603
|
+
const mimeType = mimeTypes[ext] || 'application/octet-stream';
|
|
604
|
+
const encodedFilename = `=?UTF-8?B?${Buffer.from(fileName).toString('base64')}?=`;
|
|
605
|
+
mimeLines.push(`--${boundary}`);
|
|
606
|
+
mimeLines.push(`Content-Type: ${mimeType}; name="${encodedFilename}"`);
|
|
607
|
+
mimeLines.push(`Content-Disposition: attachment; filename="${encodedFilename}"`);
|
|
608
|
+
mimeLines.push('Content-Transfer-Encoding: base64');
|
|
609
|
+
mimeLines.push('');
|
|
610
|
+
mimeLines.push(fileData.toString('base64'));
|
|
611
|
+
}
|
|
612
|
+
mimeLines.push(`--${boundary}--`);
|
|
613
|
+
}
|
|
614
|
+
const rawMessage = mimeLines.join('\r\n');
|
|
615
|
+
const encodedMessage = Buffer.from(rawMessage)
|
|
616
|
+
.toString('base64')
|
|
617
|
+
.replace(/\+/g, '-')
|
|
618
|
+
.replace(/\//g, '_')
|
|
619
|
+
.replace(/=+$/, '');
|
|
620
|
+
const draft = await gmail.users.drafts.create({
|
|
621
|
+
userId: 'me',
|
|
622
|
+
requestBody: {
|
|
623
|
+
message: {
|
|
624
|
+
raw: encodedMessage,
|
|
625
|
+
threadId: reply_to_message_id ? (await gmail.users.messages.get({ userId: 'me', id: reply_to_message_id, format: 'minimal' })).data.threadId || undefined : undefined,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
const attNames = hasAttachments
|
|
630
|
+
? attachments.map(a => a.filename || path.basename(a.file_path))
|
|
631
|
+
: [];
|
|
632
|
+
return {
|
|
633
|
+
content: [{
|
|
634
|
+
type: 'text',
|
|
635
|
+
text: JSON.stringify({
|
|
636
|
+
success: true,
|
|
637
|
+
draftId: draft.data.id,
|
|
638
|
+
messageId: draft.data.message?.id,
|
|
639
|
+
to,
|
|
640
|
+
cc: cc || [],
|
|
641
|
+
subject,
|
|
642
|
+
attachments: attNames,
|
|
643
|
+
message: 'Draft created successfully. Open Gmail to review and send.',
|
|
644
|
+
}, null, 2),
|
|
645
|
+
}],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
508
648
|
default:
|
|
509
649
|
return {
|
|
510
650
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
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}=require("./portal-api-client.cjs"),{logger:logger}=require("../utils/logger"),{t:t,setLanguage:setLanguage}=require("../i18n"),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.BLUELAMP_KEYWORD||"BL-要件定義",TOKEN=process.env.BLUELAMP_TOKEN,PORTAL_URL=process.env.PORTAL_URL||"https://bluelamp-6clpzmy5pa-an.a.run.app",LANGUAGE=process.env.BLUELAMP_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
|
|
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,verifyTokenAndGetUser:verifyTokenAndGetUser,listAvailableMcps:listAvailableMcps,getMcpInfo:getMcpInfo}=require("./portal-api-client.cjs"),{execSync:execSync}=require("child_process"),{logger:logger}=require("../utils/logger"),{t:t,setLanguage:setLanguage}=require("../i18n"),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.BLUELAMP_KEYWORD||"BL-要件定義",TOKEN=process.env.BLUELAMP_TOKEN,PORTAL_URL=process.env.PORTAL_URL||"https://bluelamp-6clpzmy5pa-an.a.run.app",LANGUAGE=process.env.BLUELAMP_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});const KAIDEN_RANKS=["皆伝プラン","編集者","管理者"];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}`),log(` .env file exists: ${fs.existsSync(dotenvPath)}`);class BlueLampVSCodeServer{constructor(){this.server=new Server({name:"bluelamp",version:"1.0.0"},{capabilities:{tools:{}}}),this.setupHandlers()}setupHandlers(){this.server.setRequestHandler(ListToolsRequestSchema,async()=>{log("[MCP Server] ListTools request received");const e={type:"object",properties:{keyword:{type:"string",description:t("mcp.keywordDescription")}},required:["keyword"]};return{tools:[{name:"load_knowledge",description:t("mcp.toolDescriptionLoadKnowledge"),annotations:{readOnlyHint:!0,idempotentHint:!0},inputSchema:e},{name:"list_mcps",description:"インストール可能なMCPサーバーの一覧を表示します(皆伝プラン以上)",annotations:{readOnlyHint:!0,idempotentHint:!0},inputSchema:{type:"object",properties:{}}},{name:"install_mcp",description:"MCPサーバーをサーバーからダウンロードしてローカルにインストールし、Claude Codeの設定に自動追加します",inputSchema:{type:"object",properties:{mcp_id:{type:"string",description:"インストールするMCPのID(list_mcpsで確認可能)"},env_vars:{type:"object",description:"環境変数(MCPが必要とする環境変数をキー・バリューで指定)",additionalProperties:{type:"string"}}},required:["mcp_id"]}}]}}),this.server.setRequestHandler(CallToolRequestSchema,async e=>{const{name:n}=e.params;if("load_knowledge"===n){const{keyword:n}=e.params.arguments;log(`[MCP Server] load_knowledge called with keyword: ${n}`),log("[MCP Server] TOKEN: "+(TOKEN?TOKEN.substring(0,20)+"...":"missing")),log(`[MCP Server] PORTAL_URL: ${PORTAL_URL}`),log(`[MCP Server] LANGUAGE: ${LANGUAGE}`);try{const e=await getPromptFromPortal(n,TOKEN,LANGUAGE);if(!e||!e.success){const r=e?.error||t("mcp.knowledgeNotFound",{keyword:n});return log(`[MCP Server] Knowledge loading failed: ${r}`),{content:[{type:"text",text:`❌ ${t("mcp.portalApiFailed",{error:r})}`}],isError:!0}}const r="ja"!==LANGUAGE?"\n\n[Note] The user's preferred language is English.":"";return{content:[{type:"text",text:`✅ ${t("mcp.knowledgeLoadSuccess")}\n\n${e.knowledge}${r}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n${t("mcp.knowledgeApplied")}`}]}}catch(e){return log(`[MCP Server] Exception in load_knowledge: ${e.message}`),{content:[{type:"text",text:`❌ ${t("mcp.knowledgeLoadError",{error:e.message})}`}],isError:!0}}}if("list_mcps"===n){log("[MCP Server] list_mcps called");try{const e=await listAvailableMcps({},TOKEN,LANGUAGE);if(!e.success)return{content:[{type:"text",text:`❌ カタログ取得エラー: ${e.error}\nブルーランプにログインしてください。`}],isError:!0};const t=(e.data||[]).filter(e=>e.hasAccess);return 0===t.length?{content:[{type:"text",text:"現在インストール可能なMCPはありません。\nプランのアップグレードが必要な場合があります。"}]}:{content:[{type:"text",text:`✅ インストール可能なMCPサーバー一覧\n\n${t.map(e=>{const t=e.toolCount?`ツール数: ${e.toolCount}`:"";return`📦 ${e.name} (ID: ${e.mcpId})\n ${e.description}\n ${t}`}).join("\n\n")}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nインストールするには install_mcp ツールを使用してください。\n必要な環境変数は各MCPの詳細で確認できます。`}]}}catch(e){return log(`[MCP Server] list_mcps error: ${e.message}`),{content:[{type:"text",text:`❌ エラー: ${e.message}`}],isError:!0}}}if("install_mcp"===n){const{mcp_id:t,env_vars:n={}}=e.params.arguments;log(`[MCP Server] install_mcp called: mcp_id=${t}`);try{const e=(...e)=>import("node-fetch").then(({default:t})=>t(...e)),r=`${PORTAL_URL}/api/mcp/${encodeURIComponent(t)}/download`;log(`[MCP Server] Downloading from: ${r}`);const o=await e(r,{method:"GET",headers:{"X-CLI-Token":TOKEN,"Content-Type":"application/json","User-Agent":"BlueLamp-MCP/1.0"}});if(!o.ok){const e=await o.text();let t=`HTTP ${o.status}`;try{t=JSON.parse(e).error||t}catch(e){}return{content:[{type:"text",text:`❌ ダウンロードエラー: ${t}`}],isError:!0}}const s=await o.json();if(!s.success||!s.data)return{content:[{type:"text",text:"❌ ダウンロードデータが不正です"}],isError:!0};const{files:a,packageSource:i,name:c}=s.data,l=a.find(e=>"manifest.json"===e.path);let p={};l&&(p=JSON.parse(l.content));const d=i.requiredEnvVars||p.requiredEnvVars||[],g=d.filter(e=>!n[e]);if(g.length>0){const e=i.optionalEnvVars||p.optionalEnvVars||[],t=e.length>0?`\n任意: ${e.join(", ")}`:"";return{content:[{type:"text",text:`❌ 以下の環境変数が必要です: ${g.join(", ")}${t}\n\ninstall_mcp を再度呼び出す際に env_vars パラメータで指定してください。`}],isError:!0}}const m=path.join(os.homedir(),".claude","mcp-servers",t);fs.mkdirSync(m,{recursive:!0});for(const e of a){if("manifest.json"===e.path)continue;const t=path.join(m,e.path),n=path.dirname(t);fs.mkdirSync(n,{recursive:!0}),fs.writeFileSync(t,e.content)}if(log(`[MCP Server] Files written to ${m}`),(i.needsNpmInstall||p.needsNpmInstall)&&fs.existsSync(path.join(m,"package.json"))){log("[MCP Server] Running npm install...");try{execSync("npm install --production",{cwd:m,stdio:"pipe",timeout:12e4}),log("[MCP Server] npm install completed")}catch(e){return{content:[{type:"text",text:`❌ npm install に失敗しました: ${e.message}\n\n手動でインストールしてください:\ncd ${m} && npm install`}],isError:!0}}}const u=i.entryPoint||p.entryPoint||"index.js",v=i.configTarget||p.configTarget||"home-mcp",S=path.join(m,u),y={};for(const e of d)n[e]&&(y[e]=n[e]);const h=i.optionalEnvVars||p.optionalEnvVars||[];for(const e of h)n[e]&&(y[e]=n[e]);const L={type:"stdio",command:"node",args:[S],...Object.keys(y).length>0?{env:y}:{}};let f;f="claude-json"===v?path.join(os.homedir(),".claude.json"):path.join(os.homedir(),".mcp.json");let E={};fs.existsSync(f)&&(E=JSON.parse(fs.readFileSync(f,"utf8"))),E.mcpServers=E.mcpServers||{},E.mcpServers[t]=L,fs.writeFileSync(f,JSON.stringify(E,null,2)),log(`[MCP Server] ${t} installed and configured in ${f}`);const P=i.installNotes||p.installNotes||"";return{content:[{type:"text",text:`✅ ${c||t} をインストールしました!\n\n📁 インストール先: ${m}\n📁 設定ファイル: ${f}${Object.keys(y).length>0?"\n🔧 設定された環境変数:\n"+Object.keys(y).map(e=>` - ${e}: ${y[e].substring(0,30)}...`).join("\n"):""}\n\n⚡ Claude Codeを再起動すると使用可能になります。\n${P?"\n📝 "+P:""}`}]}}catch(e){return log(`[MCP Server] install_mcp error: ${e.message}`),{content:[{type:"text",text:`❌ インストールエラー: ${e.message}`}],isError:!0}}}return{content:[{type:"text",text:`Unknown tool: ${e.params.name}`}],isError:!0}})}async run(){const e=new StdioServerTransport;if(await this.server.connect(e),logger.info("BlueLamp VSCode MCP server started"),KEYWORD&&TOKEN){log(`[MCP Server] Auto-loading knowledge for keyword: ${KEYWORD} (lang: ${LANGUAGE})`);const e=await getPromptFromPortal(KEYWORD,TOKEN,LANGUAGE);e&&e.success?log(`[MCP Server] Auto-load successful: ${e.title}`):log(`[MCP Server] Auto-load failed for keyword: ${KEYWORD}`)}}}const server=new BlueLampVSCodeServer;server.run().catch(e=>logger.error(e.message));
|