bluelamp 4.1.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bluelamp-vscode-base +1 -1
- package/lib/auth/InteractiveLogin.js +1 -1
- package/lib/auth/PortalAuthClient.js +1 -1
- package/lib/auth/TokenManager.js +1 -1
- package/lib/mcp-gmail/credentials.json +1 -0
- package/lib/mcp-gmail/index.js +547 -0
- package/lib/mcp-password-manager/index.js +248 -0
- package/lib/mcp-server/portal-api-client.cjs +1 -1
- package/package.json +5 -3
- package/scripts/playwright-launch.js +1 -1
- package/scripts/setup-mcps.js +2 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import initSqlJs from "sql.js";
|
|
10
|
+
const ALGORITHM = "aes-256-gcm";
|
|
11
|
+
const VAULT_DIR = join(homedir(), ".musuhi");
|
|
12
|
+
const DB_PATH = join(VAULT_DIR, "vault.db");
|
|
13
|
+
const KEY_PATH = join(VAULT_DIR, "vault.key");
|
|
14
|
+
function ensureVaultDir() {
|
|
15
|
+
if (!existsSync(VAULT_DIR)) {
|
|
16
|
+
mkdirSync(VAULT_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function getEncryptionKey() {
|
|
20
|
+
// 1. Environment variable takes priority (backward compat)
|
|
21
|
+
const envKey = process.env.VAULT_ENCRYPTION_KEY;
|
|
22
|
+
if (envKey) {
|
|
23
|
+
const buf = Buffer.from(envKey, "hex");
|
|
24
|
+
if (buf.length !== 32) {
|
|
25
|
+
throw new Error("VAULT_ENCRYPTION_KEY must be 32 bytes (64 hex characters)");
|
|
26
|
+
}
|
|
27
|
+
return buf;
|
|
28
|
+
}
|
|
29
|
+
// 2. Read from key file, or auto-generate on first run
|
|
30
|
+
ensureVaultDir();
|
|
31
|
+
if (existsSync(KEY_PATH)) {
|
|
32
|
+
const hex = readFileSync(KEY_PATH, "utf-8").trim();
|
|
33
|
+
const buf = Buffer.from(hex, "hex");
|
|
34
|
+
if (buf.length !== 32) {
|
|
35
|
+
throw new Error(`Invalid key file at ${KEY_PATH}. Delete it to regenerate.`);
|
|
36
|
+
}
|
|
37
|
+
return buf;
|
|
38
|
+
}
|
|
39
|
+
// 3. First run: generate and save
|
|
40
|
+
const newKey = randomBytes(32);
|
|
41
|
+
writeFileSync(KEY_PATH, newKey.toString("hex"), { mode: 0o600 });
|
|
42
|
+
return newKey;
|
|
43
|
+
}
|
|
44
|
+
let _db = null;
|
|
45
|
+
async function getDB() {
|
|
46
|
+
if (_db)
|
|
47
|
+
return _db;
|
|
48
|
+
ensureVaultDir();
|
|
49
|
+
const SQL = await initSqlJs();
|
|
50
|
+
if (existsSync(DB_PATH)) {
|
|
51
|
+
const buffer = readFileSync(DB_PATH);
|
|
52
|
+
_db = new SQL.Database(buffer);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
_db = new SQL.Database();
|
|
56
|
+
}
|
|
57
|
+
_db.run(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS vault_entries (
|
|
59
|
+
service TEXT PRIMARY KEY,
|
|
60
|
+
username TEXT NOT NULL,
|
|
61
|
+
encrypted TEXT NOT NULL,
|
|
62
|
+
iv TEXT NOT NULL,
|
|
63
|
+
auth_tag TEXT NOT NULL,
|
|
64
|
+
note TEXT,
|
|
65
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
return _db;
|
|
69
|
+
}
|
|
70
|
+
function saveDB(db) {
|
|
71
|
+
const data = db.export();
|
|
72
|
+
writeFileSync(DB_PATH, Buffer.from(data), { mode: 0o600 });
|
|
73
|
+
}
|
|
74
|
+
function encrypt(plaintext) {
|
|
75
|
+
const key = getEncryptionKey();
|
|
76
|
+
const iv = randomBytes(16);
|
|
77
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
78
|
+
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
79
|
+
encrypted += cipher.final("hex");
|
|
80
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
81
|
+
return { encrypted, iv: iv.toString("hex"), authTag };
|
|
82
|
+
}
|
|
83
|
+
function decrypt(encrypted, ivHex, authTagHex) {
|
|
84
|
+
const key = getEncryptionKey();
|
|
85
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
86
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
87
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
88
|
+
decipher.setAuthTag(authTag);
|
|
89
|
+
let decrypted = decipher.update(encrypted, "hex", "utf-8");
|
|
90
|
+
decrypted += decipher.final("utf-8");
|
|
91
|
+
return decrypted;
|
|
92
|
+
}
|
|
93
|
+
const server = new Server({ name: "password-manager", version: "3.1.0" }, { capabilities: { tools: {} } });
|
|
94
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
95
|
+
tools: [
|
|
96
|
+
{
|
|
97
|
+
name: "store_password",
|
|
98
|
+
description: "サービス名・ユーザー名・パスワード/APIキーを暗号化してローカルに保存",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
service: { type: "string", description: "サービス名(例: OpenAI, Google)" },
|
|
103
|
+
username: { type: "string", description: "ユーザー名・メールアドレス" },
|
|
104
|
+
password: { type: "string", description: "パスワード・APIキー・秘密鍵" },
|
|
105
|
+
note: { type: "string", description: "メモ(任意)" },
|
|
106
|
+
},
|
|
107
|
+
required: ["service", "username", "password"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "get_password",
|
|
112
|
+
description: "サービス名で検索し、復号したパスワード/APIキーを返す",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
service: { type: "string", description: "サービス名" },
|
|
117
|
+
},
|
|
118
|
+
required: ["service"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "list_passwords",
|
|
123
|
+
description: "保存済みサービス一覧を表示(パスワードは非表示)",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "delete_password",
|
|
131
|
+
description: "指定サービスのパスワードを削除",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
service: { type: "string", description: "削除するサービス名" },
|
|
136
|
+
},
|
|
137
|
+
required: ["service"],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}));
|
|
142
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
143
|
+
const { name, arguments: args } = request.params;
|
|
144
|
+
try {
|
|
145
|
+
const db = await getDB();
|
|
146
|
+
switch (name) {
|
|
147
|
+
case "store_password": {
|
|
148
|
+
const { service, username, password, note } = args;
|
|
149
|
+
const { encrypted, iv, authTag } = encrypt(password);
|
|
150
|
+
db.run(`INSERT INTO vault_entries (service, username, encrypted, iv, auth_tag, note, updated_at)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
152
|
+
ON CONFLICT (service) DO UPDATE SET
|
|
153
|
+
username = excluded.username,
|
|
154
|
+
encrypted = excluded.encrypted,
|
|
155
|
+
iv = excluded.iv,
|
|
156
|
+
auth_tag = excluded.auth_tag,
|
|
157
|
+
note = excluded.note,
|
|
158
|
+
updated_at = datetime('now')`, [service, username, encrypted, iv, authTag, note ?? null]);
|
|
159
|
+
saveDB(db);
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: `「${service}」を保存しました(ユーザー: ${username})` }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case "get_password": {
|
|
165
|
+
const { service } = args;
|
|
166
|
+
const stmt = db.prepare("SELECT * FROM vault_entries WHERE service = ?");
|
|
167
|
+
stmt.bind([service]);
|
|
168
|
+
if (!stmt.step()) {
|
|
169
|
+
stmt.free();
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: `「${service}」は見つかりません` }],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const entry = stmt.getAsObject();
|
|
176
|
+
stmt.free();
|
|
177
|
+
const password = decrypt(entry.encrypted, entry.iv, entry.auth_tag);
|
|
178
|
+
const lines = [
|
|
179
|
+
`${service}`,
|
|
180
|
+
` ユーザー名: ${entry.username}`,
|
|
181
|
+
` パスワード: ${password}`,
|
|
182
|
+
...(entry.note ? [` メモ: ${entry.note}`] : []),
|
|
183
|
+
` 更新日時: ${entry.updated_at}`,
|
|
184
|
+
];
|
|
185
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
186
|
+
}
|
|
187
|
+
case "list_passwords": {
|
|
188
|
+
const rows = [];
|
|
189
|
+
const stmt = db.prepare("SELECT service, username, note, updated_at FROM vault_entries ORDER BY service");
|
|
190
|
+
while (stmt.step()) {
|
|
191
|
+
rows.push(stmt.getAsObject());
|
|
192
|
+
}
|
|
193
|
+
stmt.free();
|
|
194
|
+
if (rows.length === 0) {
|
|
195
|
+
return { content: [{ type: "text", text: "保存済みパスワードはありません" }] };
|
|
196
|
+
}
|
|
197
|
+
const lines = [`保存済み一覧(${rows.length}件)`, ""];
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
lines.push(`- ${row.service}`);
|
|
200
|
+
lines.push(` ユーザー: ${row.username}`);
|
|
201
|
+
if (row.note)
|
|
202
|
+
lines.push(` メモ: ${row.note}`);
|
|
203
|
+
lines.push(` 更新日時: ${row.updated_at}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
207
|
+
}
|
|
208
|
+
case "delete_password": {
|
|
209
|
+
const { service } = args;
|
|
210
|
+
const check = db.prepare("SELECT 1 FROM vault_entries WHERE service = ?");
|
|
211
|
+
check.bind([service]);
|
|
212
|
+
const exists = check.step();
|
|
213
|
+
check.free();
|
|
214
|
+
if (!exists) {
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: `「${service}」は見つかりません` }],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
db.run("DELETE FROM vault_entries WHERE service = ?", [service]);
|
|
221
|
+
saveDB(db);
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: `「${service}」を削除しました` }],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
229
|
+
isError: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: `エラー: ${message}` }],
|
|
237
|
+
isError: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
async function main() {
|
|
242
|
+
const transport = new StdioServerTransport();
|
|
243
|
+
await server.connect(transport);
|
|
244
|
+
}
|
|
245
|
+
main().catch((error) => {
|
|
246
|
+
console.error("Server error:", error);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|