@rui.branco/1password-mcp 0.1.1 → 0.1.3
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 +27 -16
- package/index.js +791 -620
- package/package.json +2 -2
- package/setup.js +93 -8
package/index.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// 1Password MCP Server
|
|
5
|
+
//
|
|
6
|
+
// Exposes 1Password vault/item operations as MCP tools for Claude Code. Uses
|
|
7
|
+
// the official @1password/sdk with either a service-account token or desktop
|
|
8
|
+
// app auth (delegating unlock to 1Password 8 via DesktopAuth).
|
|
9
|
+
//
|
|
10
|
+
// Multi-instance: several 1Password accounts can be configured; each tool
|
|
11
|
+
// accepts an optional `instance` parameter to pick which one to hit.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
// Handle `1password-mcp setup` subcommand
|
|
4
15
|
if (process.argv[2] === "setup") {
|
|
5
16
|
require("./setup.js");
|
|
6
17
|
return;
|
|
@@ -22,7 +33,10 @@ const { spawn, execSync } = require("child_process");
|
|
|
22
33
|
const PKG_VERSION = require("./package.json").version;
|
|
23
34
|
const INTEGRATION_NAME = "1password-mcp";
|
|
24
35
|
|
|
25
|
-
//
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Auto-update — same pattern as jira-mcp: check GitHub for a newer commit on
|
|
38
|
+
// every start, install in a detached background process if found.
|
|
39
|
+
// ============================================================================
|
|
26
40
|
const GITHUB_REPO = "rui-branco/1password-mcp";
|
|
27
41
|
const INSTALLED_SHA_FILE = path.join(__dirname, ".installed-sha");
|
|
28
42
|
try {
|
|
@@ -47,57 +61,99 @@ try {
|
|
|
47
61
|
);
|
|
48
62
|
child.unref();
|
|
49
63
|
}
|
|
50
|
-
} catch {
|
|
64
|
+
} catch {
|
|
65
|
+
/* offline or git not available — no-op */
|
|
66
|
+
}
|
|
51
67
|
|
|
52
|
-
//
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Config
|
|
70
|
+
// ============================================================================
|
|
53
71
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
72
|
+
const CONFIG_DIR = path.join(process.env.HOME, ".config/1password-mcp");
|
|
73
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
fs.mkdirSync(
|
|
75
|
+
function ensureConfigDir() {
|
|
76
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
function loadConfigFile() {
|
|
62
80
|
try {
|
|
63
|
-
return JSON.parse(fs.readFileSync(
|
|
81
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
64
82
|
} catch {
|
|
65
83
|
return {};
|
|
66
84
|
}
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
function saveConfigFile(cfg) {
|
|
70
|
-
|
|
88
|
+
ensureConfigDir();
|
|
89
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
71
90
|
try {
|
|
72
|
-
fs.chmodSync(
|
|
73
|
-
} catch {
|
|
91
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
92
|
+
} catch {
|
|
93
|
+
/* chmod can fail on non-POSIX filesystems — non-fatal */
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
97
|
+
ensureConfigDir();
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Normalize a persisted config entry into the runtime instance shape.
|
|
101
|
+
* Accepts legacy records that only had a token and infers authType.
|
|
102
|
+
*/
|
|
103
|
+
function normalizeInstance(raw) {
|
|
104
|
+
return {
|
|
105
|
+
name: raw.name,
|
|
106
|
+
authType: raw.authType || (raw.token ? "serviceAccount" : "desktop"),
|
|
107
|
+
token: raw.token,
|
|
108
|
+
accountName: raw.accountName,
|
|
109
|
+
description: raw.description || "",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
83
112
|
|
|
84
|
-
const
|
|
85
|
-
|
|
113
|
+
const rawConfig = fs.existsSync(CONFIG_PATH)
|
|
114
|
+
? loadConfigFile()
|
|
115
|
+
: { instances: [] };
|
|
116
|
+
|
|
117
|
+
let instances = (rawConfig.instances || []).map(normalizeInstance);
|
|
118
|
+
|
|
119
|
+
let defaultInstance =
|
|
120
|
+
(rawConfig.defaultInstance &&
|
|
121
|
+
instances.find((i) => i.name === rawConfig.defaultInstance)) ||
|
|
86
122
|
instances[0] ||
|
|
87
123
|
null;
|
|
88
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Resolve an instance by name, falling back to the default if the name is
|
|
127
|
+
* omitted or unknown. Returns null only when no instances are configured at all.
|
|
128
|
+
*/
|
|
89
129
|
function getInstanceByName(name) {
|
|
90
130
|
if (!name) return defaultInstance;
|
|
91
131
|
return instances.find((i) => i.name === name) || defaultInstance;
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
//
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// 1Password SDK client
|
|
136
|
+
// ============================================================================
|
|
95
137
|
|
|
96
|
-
// Cache SDK clients per instance
|
|
97
|
-
//
|
|
98
|
-
// would be wasteful.
|
|
138
|
+
// Cache SDK clients per instance — createClient spins up a WASM runtime and
|
|
139
|
+
// negotiates an auth session, so we want exactly one per process lifetime.
|
|
99
140
|
const clientCache = new Map();
|
|
100
141
|
|
|
142
|
+
function buildAuth(instance) {
|
|
143
|
+
if (instance.authType === "desktop") {
|
|
144
|
+
if (!instance.accountName) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Instance "${instance.name}" is desktop-auth but has no accountName. Re-run setup.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return new sdk.DesktopAuth(instance.accountName);
|
|
150
|
+
}
|
|
151
|
+
if (!instance.token) {
|
|
152
|
+
throw new Error(`Instance "${instance.name}" has no service account token.`);
|
|
153
|
+
}
|
|
154
|
+
return instance.token;
|
|
155
|
+
}
|
|
156
|
+
|
|
101
157
|
async function getClient(instance) {
|
|
102
158
|
if (!instance) {
|
|
103
159
|
throw new Error(
|
|
@@ -105,11 +161,9 @@ async function getClient(instance) {
|
|
|
105
161
|
);
|
|
106
162
|
}
|
|
107
163
|
if (clientCache.has(instance.name)) return clientCache.get(instance.name);
|
|
108
|
-
|
|
109
|
-
throw new Error(`Instance "${instance.name}" has no service account token.`);
|
|
110
|
-
}
|
|
164
|
+
|
|
111
165
|
const client = await sdk.createClient({
|
|
112
|
-
auth: instance
|
|
166
|
+
auth: buildAuth(instance),
|
|
113
167
|
integrationName: INTEGRATION_NAME,
|
|
114
168
|
integrationVersion: `v${PKG_VERSION}`,
|
|
115
169
|
});
|
|
@@ -121,37 +175,54 @@ function invalidateClient(name) {
|
|
|
121
175
|
clientCache.delete(name);
|
|
122
176
|
}
|
|
123
177
|
|
|
124
|
-
//
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Helpers — vault/item resolution, redaction, formatting
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
async function collect(asyncIterable) {
|
|
183
|
+
const out = [];
|
|
184
|
+
for await (const x of asyncIterable) out.push(x);
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
125
187
|
|
|
126
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a vault from a reference that may be an id, exact title (case-insensitive),
|
|
190
|
+
* or prefix of a title. Returns null when nothing matches.
|
|
191
|
+
*/
|
|
127
192
|
async function resolveVault(client, vaultRef) {
|
|
128
193
|
if (!vaultRef) return null;
|
|
129
|
-
const vaults =
|
|
130
|
-
for await (const v of await client.vaults.list()) vaults.push(v);
|
|
194
|
+
const vaults = await collect(await client.vaults.list());
|
|
131
195
|
const byId = vaults.find((v) => v.id === vaultRef);
|
|
132
196
|
if (byId) return byId;
|
|
133
197
|
const lower = String(vaultRef).toLowerCase();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
198
|
+
return (
|
|
199
|
+
vaults.find((v) => (v.title || "").toLowerCase() === lower) ||
|
|
200
|
+
vaults.find((v) => (v.title || "").toLowerCase().startsWith(lower)) ||
|
|
201
|
+
null
|
|
202
|
+
);
|
|
139
203
|
}
|
|
140
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Same strategy as resolveVault but for items inside a specific vault.
|
|
207
|
+
*/
|
|
141
208
|
async function resolveItem(client, vault, itemRef) {
|
|
142
209
|
if (!itemRef) return null;
|
|
143
|
-
const items =
|
|
144
|
-
for await (const it of await client.items.list(vault.id)) items.push(it);
|
|
210
|
+
const items = await collect(await client.items.list(vault.id));
|
|
145
211
|
const byId = items.find((i) => i.id === itemRef);
|
|
146
212
|
if (byId) return byId;
|
|
147
213
|
const lower = String(itemRef).toLowerCase();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
214
|
+
return (
|
|
215
|
+
items.find((i) => (i.title || "").toLowerCase() === lower) ||
|
|
216
|
+
items.find((i) => (i.title || "").toLowerCase().startsWith(lower)) ||
|
|
217
|
+
null
|
|
218
|
+
);
|
|
153
219
|
}
|
|
154
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Shape a single 1Password field for output. Concealed and TOTP values are
|
|
223
|
+
* replaced with "[REDACTED]" unless includeConcealed=true. OTP codes are only
|
|
224
|
+
* surfaced when concealed values are revealed.
|
|
225
|
+
*/
|
|
155
226
|
function redactField(field, includeConcealed) {
|
|
156
227
|
const out = {
|
|
157
228
|
id: field.id,
|
|
@@ -162,8 +233,13 @@ function redactField(field, includeConcealed) {
|
|
|
162
233
|
const isSecret = field.fieldType === "Concealed" || field.fieldType === "Totp";
|
|
163
234
|
if (!isSecret || includeConcealed) {
|
|
164
235
|
out.value = field.value;
|
|
165
|
-
if (
|
|
166
|
-
|
|
236
|
+
if (
|
|
237
|
+
field.details &&
|
|
238
|
+
field.details.type === "Otp" &&
|
|
239
|
+
field.details.content &&
|
|
240
|
+
field.details.content.code
|
|
241
|
+
) {
|
|
242
|
+
out.otp = field.details.content.code;
|
|
167
243
|
}
|
|
168
244
|
} else {
|
|
169
245
|
out.value = "[REDACTED]";
|
|
@@ -171,6 +247,10 @@ function redactField(field, includeConcealed) {
|
|
|
171
247
|
return out;
|
|
172
248
|
}
|
|
173
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Flatten a full 1Password item (as returned by client.items.get) into a
|
|
252
|
+
* stable JSON shape that's safe to display to the model.
|
|
253
|
+
*/
|
|
174
254
|
function formatItem(item, includeConcealed) {
|
|
175
255
|
return {
|
|
176
256
|
id: item.id,
|
|
@@ -178,638 +258,707 @@ function formatItem(item, includeConcealed) {
|
|
|
178
258
|
category: item.category,
|
|
179
259
|
vaultId: item.vaultId,
|
|
180
260
|
tags: item.tags || [],
|
|
181
|
-
websites: (item.websites || []).map((w) => ({
|
|
261
|
+
websites: (item.websites || []).map((w) => ({
|
|
262
|
+
url: w.url,
|
|
263
|
+
label: w.label,
|
|
264
|
+
})),
|
|
182
265
|
fields: (item.fields || []).map((f) => redactField(f, includeConcealed)),
|
|
183
266
|
sections: (item.sections || []).map((s) => ({ id: s.id, title: s.title })),
|
|
184
267
|
};
|
|
185
268
|
}
|
|
186
269
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Tool definitions (exposed via tools/list)
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
const TOOL_DEFINITIONS = [
|
|
275
|
+
{
|
|
276
|
+
name: "op_list_instances",
|
|
277
|
+
description:
|
|
278
|
+
"List configured 1Password instances (service accounts or desktop). Tokens are never returned.",
|
|
279
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "op_add_instance",
|
|
283
|
+
description:
|
|
284
|
+
"Add or update a 1Password instance. Two auth modes: (1) 'serviceAccount' — provide token (ops_...). (2) 'desktop' — provide accountName (e.g. 'my.1password.com'); auth is delegated to the installed 1Password 8 desktop app via biometric unlock. Config is stored at ~/.config/1password-mcp/config.json with 0600 permissions.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
name: {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "Unique name (e.g., 'work', 'personal').",
|
|
291
|
+
},
|
|
292
|
+
authType: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "'serviceAccount' (default) or 'desktop'.",
|
|
295
|
+
},
|
|
296
|
+
token: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description:
|
|
299
|
+
"Service account token (ops_...) — required for authType=serviceAccount.",
|
|
300
|
+
},
|
|
301
|
+
accountName: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description:
|
|
304
|
+
"1Password account (e.g. 'my.1password.com' or shorthand) — required for authType=desktop. Uses the logged-in 1Password 8 app.",
|
|
305
|
+
},
|
|
306
|
+
description: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Optional human-readable description.",
|
|
309
|
+
},
|
|
310
|
+
setDefault: {
|
|
311
|
+
type: "boolean",
|
|
312
|
+
description: "Set this instance as the default (default: false).",
|
|
222
313
|
},
|
|
223
314
|
},
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
315
|
+
required: ["name"],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "op_remove_instance",
|
|
320
|
+
description:
|
|
321
|
+
"Remove a 1Password instance by name. Cannot remove the last one.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
name: { type: "string", description: "Instance name to remove." },
|
|
326
|
+
},
|
|
327
|
+
required: ["name"],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "op_list_vaults",
|
|
332
|
+
description:
|
|
333
|
+
"List all vaults visible to the given instance's 1Password account.",
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
instance: {
|
|
338
|
+
type: "string",
|
|
339
|
+
description: "Instance name. Uses default if omitted.",
|
|
233
340
|
},
|
|
234
341
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
342
|
+
required: [],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "op_list_items",
|
|
347
|
+
description:
|
|
348
|
+
"List items in a vault. `vault` accepts a vault id or title (case-insensitive exact, then prefix).",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
353
|
+
instance: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Instance name. Uses default if omitted.",
|
|
244
356
|
},
|
|
245
357
|
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
358
|
+
required: ["vault"],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "op_search_items",
|
|
363
|
+
description:
|
|
364
|
+
"Search items by title across vaults. If `vault` is omitted, searches every vault the account can see.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
query: {
|
|
369
|
+
type: "string",
|
|
370
|
+
description: "Case-insensitive substring of the item title.",
|
|
371
|
+
},
|
|
372
|
+
vault: {
|
|
373
|
+
type: "string",
|
|
374
|
+
description: "Optional vault id or title to scope the search.",
|
|
375
|
+
},
|
|
376
|
+
limit: {
|
|
377
|
+
type: "number",
|
|
378
|
+
description: "Max results (default 25).",
|
|
379
|
+
},
|
|
380
|
+
instance: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description: "Instance name. Uses default if omitted.",
|
|
257
383
|
},
|
|
258
384
|
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
385
|
+
required: ["query"],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "op_get_item",
|
|
390
|
+
description:
|
|
391
|
+
"Fetch a full item with all fields. By default concealed fields (passwords, TOTP) are redacted; set includeConcealed=true to return them in cleartext. `item` and `vault` accept ids or titles.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
396
|
+
item: { type: "string", description: "Item id or title." },
|
|
397
|
+
includeConcealed: {
|
|
398
|
+
type: "boolean",
|
|
399
|
+
description: "Return concealed values in cleartext (default: false).",
|
|
400
|
+
},
|
|
401
|
+
instance: {
|
|
402
|
+
type: "string",
|
|
403
|
+
description: "Instance name. Uses default if omitted.",
|
|
272
404
|
},
|
|
273
405
|
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
406
|
+
required: ["vault", "item"],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "op_get_secret",
|
|
411
|
+
description:
|
|
412
|
+
"Resolve a 1Password secret reference (op://vault/item/field) and return the value.",
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
reference: {
|
|
417
|
+
type: "string",
|
|
418
|
+
description: "Secret reference like 'op://Private/GitHub/password'.",
|
|
419
|
+
},
|
|
420
|
+
instance: {
|
|
421
|
+
type: "string",
|
|
422
|
+
description: "Instance name. Uses default if omitted.",
|
|
290
423
|
},
|
|
291
424
|
},
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
425
|
+
required: ["reference"],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "op_generate_password",
|
|
430
|
+
description:
|
|
431
|
+
"Generate a password using 1Password's generator (Random / Memorable / Pin). Does not talk to the account.",
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
type: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "'Random' (default), 'Memorable', or 'Pin'.",
|
|
438
|
+
},
|
|
439
|
+
length: {
|
|
440
|
+
type: "number",
|
|
441
|
+
description: "Length for Random/Pin (default 20/8).",
|
|
442
|
+
},
|
|
443
|
+
includeDigits: {
|
|
444
|
+
type: "boolean",
|
|
445
|
+
description: "Random only (default true).",
|
|
446
|
+
},
|
|
447
|
+
includeSymbols: {
|
|
448
|
+
type: "boolean",
|
|
449
|
+
description: "Random only (default true).",
|
|
450
|
+
},
|
|
451
|
+
wordCount: {
|
|
452
|
+
type: "number",
|
|
453
|
+
description: "Memorable only (default 5).",
|
|
454
|
+
},
|
|
455
|
+
capitalize: {
|
|
456
|
+
type: "boolean",
|
|
457
|
+
description: "Memorable only (default true).",
|
|
306
458
|
},
|
|
307
459
|
},
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
460
|
+
required: [],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: "op_create_item",
|
|
465
|
+
description:
|
|
466
|
+
"Create a new item in a vault. Fields is an array of { title, type, value, sectionId? }. Type is Text | Concealed | Totp | Email | Url | Phone.",
|
|
467
|
+
inputSchema: {
|
|
468
|
+
type: "object",
|
|
469
|
+
properties: {
|
|
470
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
471
|
+
title: { type: "string", description: "Item title." },
|
|
472
|
+
category: {
|
|
473
|
+
type: "string",
|
|
474
|
+
description:
|
|
475
|
+
"Item category (default: Login). One of: Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
|
|
476
|
+
},
|
|
477
|
+
fields: {
|
|
478
|
+
type: "array",
|
|
479
|
+
description:
|
|
480
|
+
"Field definitions: [{ title, type, value, sectionId? }].",
|
|
481
|
+
items: {
|
|
482
|
+
type: "object",
|
|
483
|
+
properties: {
|
|
484
|
+
title: { type: "string" },
|
|
485
|
+
type: { type: "string" },
|
|
486
|
+
value: { type: "string" },
|
|
487
|
+
sectionId: { type: "string" },
|
|
318
488
|
},
|
|
319
|
-
|
|
320
|
-
includeDigits: { type: "boolean", description: "Random only (default true)." },
|
|
321
|
-
includeSymbols: { type: "boolean", description: "Random only (default true)." },
|
|
322
|
-
wordCount: { type: "number", description: "Memorable only (default 5)." },
|
|
323
|
-
capitalize: { type: "boolean", description: "Memorable only (default true)." },
|
|
489
|
+
required: ["title", "type", "value"],
|
|
324
490
|
},
|
|
325
|
-
required: [],
|
|
326
491
|
},
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
vault: { type: "string", description: "Vault id or title." },
|
|
336
|
-
title: { type: "string", description: "Item title." },
|
|
337
|
-
category: {
|
|
338
|
-
type: "string",
|
|
339
|
-
description:
|
|
340
|
-
"Item category (default: Login). One of: Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
|
|
341
|
-
},
|
|
342
|
-
fields: {
|
|
343
|
-
type: "array",
|
|
344
|
-
description: "Field definitions: [{ title, type, value, sectionId? }].",
|
|
345
|
-
items: {
|
|
346
|
-
type: "object",
|
|
347
|
-
properties: {
|
|
348
|
-
title: { type: "string" },
|
|
349
|
-
type: { type: "string" },
|
|
350
|
-
value: { type: "string" },
|
|
351
|
-
sectionId: { type: "string" },
|
|
352
|
-
},
|
|
353
|
-
required: ["title", "type", "value"],
|
|
354
|
-
},
|
|
492
|
+
tags: { type: "array", items: { type: "string" } },
|
|
493
|
+
websites: {
|
|
494
|
+
type: "array",
|
|
495
|
+
items: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
url: { type: "string" },
|
|
499
|
+
label: { type: "string" },
|
|
355
500
|
},
|
|
356
|
-
|
|
357
|
-
websites: {
|
|
358
|
-
type: "array",
|
|
359
|
-
items: {
|
|
360
|
-
type: "object",
|
|
361
|
-
properties: {
|
|
362
|
-
url: { type: "string" },
|
|
363
|
-
label: { type: "string" },
|
|
364
|
-
},
|
|
365
|
-
required: ["url"],
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
notes: { type: "string", description: "Optional notes (stored as a Text field called 'notesPlain')." },
|
|
369
|
-
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
501
|
+
required: ["url"],
|
|
370
502
|
},
|
|
371
|
-
|
|
503
|
+
},
|
|
504
|
+
notes: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description:
|
|
507
|
+
"Optional notes (stored as a Text field called 'notesPlain').",
|
|
508
|
+
},
|
|
509
|
+
instance: {
|
|
510
|
+
type: "string",
|
|
511
|
+
description: "Instance name. Uses default if omitted.",
|
|
372
512
|
},
|
|
373
513
|
},
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
514
|
+
required: ["vault", "title"],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: "op_delete_item",
|
|
519
|
+
description: "Delete an item by vault + item reference.",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
524
|
+
item: { type: "string", description: "Item id or title." },
|
|
525
|
+
instance: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description: "Instance name. Uses default if omitted.",
|
|
385
528
|
},
|
|
386
529
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
530
|
+
required: ["vault", "item"],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "op_create_vault",
|
|
535
|
+
description: "Create a new vault.",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
title: { type: "string", description: "Vault title." },
|
|
540
|
+
description: {
|
|
541
|
+
type: "string",
|
|
542
|
+
description: "Optional description.",
|
|
543
|
+
},
|
|
544
|
+
instance: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Instance name. Uses default if omitted.",
|
|
398
547
|
},
|
|
399
548
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
549
|
+
required: ["title"],
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "op_delete_vault",
|
|
554
|
+
description: "Delete a vault by id or title.",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
559
|
+
instance: {
|
|
560
|
+
type: "string",
|
|
561
|
+
description: "Instance name. Uses default if omitted.",
|
|
410
562
|
},
|
|
411
563
|
},
|
|
412
|
-
|
|
564
|
+
required: ["vault"],
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// Handlers
|
|
571
|
+
//
|
|
572
|
+
// Each handler is keyed by tool name and receives the tool arguments. It
|
|
573
|
+
// should return a plain string (rendered as text content by the dispatcher)
|
|
574
|
+
// or throw on failure. The dispatcher handles MCP wrapping and error shape.
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
async function addInstanceHandler(args) {
|
|
578
|
+
const instName = args.name.trim();
|
|
579
|
+
const authType = args.authType || (args.token ? "serviceAccount" : "desktop");
|
|
580
|
+
|
|
581
|
+
if (authType === "serviceAccount") {
|
|
582
|
+
if (!args.token) {
|
|
583
|
+
throw new Error("serviceAccount authType requires a token.");
|
|
584
|
+
}
|
|
585
|
+
if (!/^ops_[A-Za-z0-9_\-]+/.test(args.token)) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
"Token does not look like a 1Password service account token (expected prefix 'ops_').",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
} else if (authType === "desktop") {
|
|
591
|
+
if (!args.accountName) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
"desktop authType requires accountName (e.g. 'my.1password.com').",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
throw new Error(`Unknown authType "${authType}".`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Validate by creating a client and listing one vault.
|
|
601
|
+
const probeAuth =
|
|
602
|
+
authType === "desktop" ? new sdk.DesktopAuth(args.accountName) : args.token;
|
|
603
|
+
const tempClient = await sdk.createClient({
|
|
604
|
+
auth: probeAuth,
|
|
605
|
+
integrationName: INTEGRATION_NAME,
|
|
606
|
+
integrationVersion: `v${PKG_VERSION}`,
|
|
607
|
+
});
|
|
608
|
+
const probe = [];
|
|
609
|
+
for await (const v of await tempClient.vaults.list()) {
|
|
610
|
+
probe.push(v);
|
|
611
|
+
if (probe.length >= 1) break;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const newInst = {
|
|
615
|
+
name: instName,
|
|
616
|
+
authType,
|
|
617
|
+
token: authType === "serviceAccount" ? args.token : undefined,
|
|
618
|
+
accountName: authType === "desktop" ? args.accountName : undefined,
|
|
619
|
+
description: args.description || "",
|
|
413
620
|
};
|
|
414
|
-
|
|
621
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
622
|
+
if (existingIdx >= 0) instances[existingIdx] = newInst;
|
|
623
|
+
else instances.push(newInst);
|
|
624
|
+
invalidateClient(instName);
|
|
625
|
+
|
|
626
|
+
const savedConfig = loadConfigFile();
|
|
627
|
+
if (!savedConfig.instances) savedConfig.instances = [];
|
|
628
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
629
|
+
const toSave = { name: instName, authType };
|
|
630
|
+
if (authType === "serviceAccount") toSave.token = args.token;
|
|
631
|
+
if (authType === "desktop") toSave.accountName = args.accountName;
|
|
632
|
+
if (args.description) toSave.description = args.description;
|
|
633
|
+
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
634
|
+
else savedConfig.instances.push(toSave);
|
|
635
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
636
|
+
savedConfig.defaultInstance = instName;
|
|
637
|
+
defaultInstance = newInst;
|
|
638
|
+
}
|
|
639
|
+
saveConfigFile(savedConfig);
|
|
415
640
|
|
|
416
|
-
|
|
417
|
-
const
|
|
641
|
+
const verb = existingIdx >= 0 ? "Updated" : "Added";
|
|
642
|
+
const suffix = args.setDefault ? " Set as default." : "";
|
|
643
|
+
return `${verb} instance "${instName}" (${authType}).${suffix}`;
|
|
644
|
+
}
|
|
418
645
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
return { content: [{ type: "text", text }] };
|
|
646
|
+
async function removeInstanceHandler(args) {
|
|
647
|
+
if (instances.length <= 1) {
|
|
648
|
+
throw new Error("Cannot remove the last remaining instance.");
|
|
649
|
+
}
|
|
650
|
+
const idx = instances.findIndex((i) => i.name === args.name);
|
|
651
|
+
if (idx < 0) throw new Error(`Instance "${args.name}" not found.`);
|
|
652
|
+
|
|
653
|
+
instances.splice(idx, 1);
|
|
654
|
+
invalidateClient(args.name);
|
|
655
|
+
|
|
656
|
+
const savedConfig = loadConfigFile();
|
|
657
|
+
if (savedConfig.instances) {
|
|
658
|
+
savedConfig.instances = savedConfig.instances.filter(
|
|
659
|
+
(i) => i.name !== args.name,
|
|
660
|
+
);
|
|
661
|
+
if (savedConfig.defaultInstance === args.name) {
|
|
662
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
663
|
+
defaultInstance = instances[0] || null;
|
|
439
664
|
}
|
|
665
|
+
saveConfigFile(savedConfig);
|
|
666
|
+
}
|
|
667
|
+
return `Removed instance "${args.name}".`;
|
|
668
|
+
}
|
|
440
669
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
670
|
+
function listInstancesHandler() {
|
|
671
|
+
if (instances.length === 0) {
|
|
672
|
+
return "No instances configured. Run `1password-mcp setup` or call op_add_instance.";
|
|
673
|
+
}
|
|
674
|
+
const def = defaultInstance?.name;
|
|
675
|
+
const lines = instances.map((inst) => {
|
|
676
|
+
const isDefault = inst.name === def ? " **(default)**" : "";
|
|
677
|
+
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
678
|
+
const auth =
|
|
679
|
+
inst.authType === "desktop"
|
|
680
|
+
? ` (desktop: ${inst.accountName})`
|
|
681
|
+
: " (serviceAccount)";
|
|
682
|
+
return `- **${inst.name}**${isDefault}${auth}${desc}`;
|
|
683
|
+
});
|
|
684
|
+
return `# 1Password Instances (${instances.length})\n\n${lines.join("\n")}`;
|
|
685
|
+
}
|
|
454
686
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
for await (const v of await tempClient.vaults.list()) {
|
|
465
|
-
probe.push(v);
|
|
466
|
-
if (probe.length >= 1) break;
|
|
467
|
-
}
|
|
468
|
-
} catch (e) {
|
|
469
|
-
return {
|
|
470
|
-
content: [
|
|
471
|
-
{ type: "text", text: `Token validation failed: ${e.message}` },
|
|
472
|
-
],
|
|
473
|
-
isError: true,
|
|
474
|
-
};
|
|
475
|
-
}
|
|
687
|
+
async function listVaultsHandler(args) {
|
|
688
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
689
|
+
const vaults = await collect(await client.vaults.list());
|
|
690
|
+
if (vaults.length === 0) {
|
|
691
|
+
return "No vaults visible to this instance.";
|
|
692
|
+
}
|
|
693
|
+
const lines = vaults.map((v) => `- **${v.title}** (id: ${v.id})`);
|
|
694
|
+
return `# Vaults (${vaults.length})\n\n${lines.join("\n")}`;
|
|
695
|
+
}
|
|
476
696
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
};
|
|
482
|
-
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
483
|
-
if (existingIdx >= 0) instances[existingIdx] = newInst;
|
|
484
|
-
else instances.push(newInst);
|
|
485
|
-
invalidateClient(instName);
|
|
486
|
-
|
|
487
|
-
const savedConfig = loadConfigFile();
|
|
488
|
-
if (!savedConfig.instances) savedConfig.instances = [];
|
|
489
|
-
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
490
|
-
const toSave = { name: instName, token: args.token };
|
|
491
|
-
if (args.description) toSave.description = args.description;
|
|
492
|
-
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
493
|
-
else savedConfig.instances.push(toSave);
|
|
494
|
-
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
495
|
-
savedConfig.defaultInstance = instName;
|
|
496
|
-
}
|
|
497
|
-
saveConfigFile(savedConfig);
|
|
697
|
+
async function listItemsHandler(args) {
|
|
698
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
699
|
+
const vault = await resolveVault(client, args.vault);
|
|
700
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
498
701
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
702
|
+
const items = await collect(await client.items.list(vault.id));
|
|
703
|
+
if (items.length === 0) {
|
|
704
|
+
return `No items in vault "${vault.title}".`;
|
|
705
|
+
}
|
|
706
|
+
const lines = items.map(
|
|
707
|
+
(i) => `- **${i.title}** (id: ${i.id}, category: ${i.category})`,
|
|
708
|
+
);
|
|
709
|
+
return `# Items in ${vault.title} (${items.length})\n\n${lines.join("\n")}`;
|
|
710
|
+
}
|
|
508
711
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
instances.splice(idx, 1);
|
|
524
|
-
invalidateClient(args.name);
|
|
525
|
-
const savedConfig = loadConfigFile();
|
|
526
|
-
if (savedConfig.instances) {
|
|
527
|
-
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== args.name);
|
|
528
|
-
if (savedConfig.defaultInstance === args.name) {
|
|
529
|
-
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
530
|
-
}
|
|
531
|
-
saveConfigFile(savedConfig);
|
|
532
|
-
}
|
|
533
|
-
return { content: [{ type: "text", text: `Removed instance "${args.name}".` }] };
|
|
534
|
-
}
|
|
712
|
+
async function searchItemsHandler(args) {
|
|
713
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
714
|
+
const limit = args.limit || 25;
|
|
715
|
+
const q = String(args.query).toLowerCase();
|
|
716
|
+
|
|
717
|
+
let targetVaults;
|
|
718
|
+
if (args.vault) {
|
|
719
|
+
const v = await resolveVault(client, args.vault);
|
|
720
|
+
if (!v) throw new Error(`Vault "${args.vault}" not found.`);
|
|
721
|
+
targetVaults = [v];
|
|
722
|
+
} else {
|
|
723
|
+
targetVaults = await collect(await client.vaults.list());
|
|
724
|
+
}
|
|
535
725
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
return { content: [{ type: "text", text: "No vaults visible to this service account." }] };
|
|
726
|
+
const hits = [];
|
|
727
|
+
for (const v of targetVaults) {
|
|
728
|
+
for await (const it of await client.items.list(v.id)) {
|
|
729
|
+
if ((it.title || "").toLowerCase().includes(q)) {
|
|
730
|
+
hits.push({ ...it, _vaultTitle: v.title });
|
|
731
|
+
if (hits.length >= limit) break;
|
|
543
732
|
}
|
|
544
|
-
let text = `# Vaults (${vaults.length})\n\n`;
|
|
545
|
-
for (const v of vaults) text += `- **${v.title}** (id: ${v.id})\n`;
|
|
546
|
-
return { content: [{ type: "text", text }] };
|
|
547
733
|
}
|
|
734
|
+
if (hits.length >= limit) break;
|
|
735
|
+
}
|
|
548
736
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
737
|
+
if (hits.length === 0) return `No items matched "${args.query}".`;
|
|
738
|
+
const lines = hits.map(
|
|
739
|
+
(it) =>
|
|
740
|
+
`- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})`,
|
|
741
|
+
);
|
|
742
|
+
return `# Search: "${args.query}" (${hits.length})\n\n${lines.join("\n")}`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function getItemHandler(args) {
|
|
746
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
747
|
+
const vault = await resolveVault(client, args.vault);
|
|
748
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
749
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
750
|
+
if (!itemRef) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const full = await client.items.get(vault.id, itemRef.id);
|
|
756
|
+
const formatted = formatItem(full, !!args.includeConcealed);
|
|
757
|
+
|
|
758
|
+
const lines = [
|
|
759
|
+
`# ${formatted.title} (${formatted.category})`,
|
|
760
|
+
"",
|
|
761
|
+
`- **Vault:** ${vault.title} (${formatted.vaultId})`,
|
|
762
|
+
`- **Item ID:** ${formatted.id}`,
|
|
763
|
+
];
|
|
764
|
+
if (formatted.tags.length) lines.push(`- **Tags:** ${formatted.tags.join(", ")}`);
|
|
765
|
+
if (formatted.websites.length) {
|
|
766
|
+
lines.push(
|
|
767
|
+
`- **Websites:** ${formatted.websites.map((w) => w.url).join(", ")}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (formatted.fields.length) {
|
|
771
|
+
lines.push("", "## Fields", "");
|
|
772
|
+
for (const f of formatted.fields) {
|
|
773
|
+
const val = f.value === undefined ? "" : f.value;
|
|
774
|
+
const otp = f.otp ? ` (OTP now: ${f.otp})` : "";
|
|
775
|
+
lines.push(`- **${f.title}** (${f.type}): ${val}${otp}`);
|
|
569
776
|
}
|
|
777
|
+
}
|
|
778
|
+
if (!args.includeConcealed) {
|
|
779
|
+
lines.push(
|
|
780
|
+
"",
|
|
781
|
+
"_Concealed fields are redacted. Call with includeConcealed=true to reveal._",
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return lines.join("\n");
|
|
785
|
+
}
|
|
570
786
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
let targetVaults = [];
|
|
578
|
-
if (args.vault) {
|
|
579
|
-
const v = await resolveVault(client, args.vault);
|
|
580
|
-
if (!v) {
|
|
581
|
-
return {
|
|
582
|
-
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
583
|
-
isError: true,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
targetVaults = [v];
|
|
587
|
-
} else {
|
|
588
|
-
for await (const v of await client.vaults.list()) targetVaults.push(v);
|
|
589
|
-
}
|
|
787
|
+
async function getSecretHandler(args) {
|
|
788
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
789
|
+
const value = await client.secrets.resolve(args.reference);
|
|
790
|
+
return `Secret for ${args.reference}:\n${value}`;
|
|
791
|
+
}
|
|
590
792
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
793
|
+
function generatePasswordHandler(args) {
|
|
794
|
+
const type = args.type || "Random";
|
|
795
|
+
let generated;
|
|
796
|
+
if (type === "Pin") {
|
|
797
|
+
generated = sdk.Secrets.generatePassword({
|
|
798
|
+
type: "Pin",
|
|
799
|
+
parameters: { length: args.length || 8 },
|
|
800
|
+
});
|
|
801
|
+
} else if (type === "Memorable") {
|
|
802
|
+
generated = sdk.Secrets.generatePassword({
|
|
803
|
+
type: "Memorable",
|
|
804
|
+
parameters: {
|
|
805
|
+
separatorType: sdk.SeparatorType.Digits,
|
|
806
|
+
capitalize: args.capitalize !== false,
|
|
807
|
+
wordListType: sdk.WordListType.FullWords,
|
|
808
|
+
wordCount: args.wordCount || 5,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
} else {
|
|
812
|
+
generated = sdk.Secrets.generatePassword({
|
|
813
|
+
type: "Random",
|
|
814
|
+
parameters: {
|
|
815
|
+
includeDigits: args.includeDigits !== false,
|
|
816
|
+
includeSymbols: args.includeSymbols !== false,
|
|
817
|
+
length: args.length || 20,
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const pwd =
|
|
822
|
+
typeof generated === "string" ? generated : generated.password || generated;
|
|
823
|
+
return `Generated (${type}):\n${pwd}`;
|
|
824
|
+
}
|
|
601
825
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
826
|
+
async function createItemHandler(args) {
|
|
827
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
828
|
+
const vault = await resolveVault(client, args.vault);
|
|
829
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
830
|
+
|
|
831
|
+
const category = sdk.ItemCategory[args.category || "Login"];
|
|
832
|
+
if (!category) throw new Error(`Unknown category "${args.category}".`);
|
|
833
|
+
|
|
834
|
+
const fields = (args.fields || []).map((f) => {
|
|
835
|
+
const ft = sdk.ItemFieldType[f.type];
|
|
836
|
+
if (!ft) throw new Error(`Unknown field type "${f.type}"`);
|
|
837
|
+
const out = {
|
|
838
|
+
id: f.title.toLowerCase().replace(/\s+/g, "_"),
|
|
839
|
+
title: f.title,
|
|
840
|
+
fieldType: ft,
|
|
841
|
+
value: f.value,
|
|
842
|
+
};
|
|
843
|
+
if (f.sectionId) out.sectionId = f.sectionId;
|
|
844
|
+
return out;
|
|
845
|
+
});
|
|
846
|
+
if (args.notes) {
|
|
847
|
+
fields.push({
|
|
848
|
+
id: "notesPlain",
|
|
849
|
+
title: "notesPlain",
|
|
850
|
+
fieldType: sdk.ItemFieldType.Text,
|
|
851
|
+
value: args.notes,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
613
854
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
isError: true,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
const full = await client.items.get(vault.id, itemRef.id);
|
|
632
|
-
const formatted = formatItem(full, !!args.includeConcealed);
|
|
633
|
-
let text = `# ${formatted.title} (${formatted.category})\n\n`;
|
|
634
|
-
text += `- **Vault:** ${vault.title} (${formatted.vaultId})\n`;
|
|
635
|
-
text += `- **Item ID:** ${formatted.id}\n`;
|
|
636
|
-
if (formatted.tags.length > 0) text += `- **Tags:** ${formatted.tags.join(", ")}\n`;
|
|
637
|
-
if (formatted.websites.length > 0) {
|
|
638
|
-
text += `- **Websites:** ${formatted.websites.map((w) => w.url).join(", ")}\n`;
|
|
639
|
-
}
|
|
640
|
-
if (formatted.fields.length > 0) {
|
|
641
|
-
text += `\n## Fields\n\n`;
|
|
642
|
-
for (const f of formatted.fields) {
|
|
643
|
-
const val = f.value === undefined ? "" : f.value;
|
|
644
|
-
text += `- **${f.title}** (${f.type}): ${val}${f.otp ? ` (OTP now: ${f.otp})` : ""}\n`;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (!args.includeConcealed) {
|
|
648
|
-
text += `\n_Concealed fields are redacted. Call with includeConcealed=true to reveal._`;
|
|
649
|
-
}
|
|
650
|
-
return { content: [{ type: "text", text }] };
|
|
651
|
-
}
|
|
855
|
+
const item = await client.items.create({
|
|
856
|
+
title: args.title,
|
|
857
|
+
category,
|
|
858
|
+
vaultId: vault.id,
|
|
859
|
+
fields,
|
|
860
|
+
tags: args.tags || [],
|
|
861
|
+
websites: (args.websites || []).map((w) => ({
|
|
862
|
+
url: w.url,
|
|
863
|
+
label: w.label || "website",
|
|
864
|
+
autofillBehavior: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
865
|
+
})),
|
|
866
|
+
});
|
|
867
|
+
return `Created item **${item.title}** (id: ${item.id}) in vault "${vault.title}".`;
|
|
868
|
+
}
|
|
652
869
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
870
|
+
async function deleteItemHandler(args) {
|
|
871
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
872
|
+
const vault = await resolveVault(client, args.vault);
|
|
873
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
874
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
875
|
+
if (!itemRef) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
await client.items.delete(vault.id, itemRef.id);
|
|
881
|
+
return `Deleted item "${itemRef.title}" from vault "${vault.title}".`;
|
|
882
|
+
}
|
|
661
883
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
} else if (type === "Memorable") {
|
|
671
|
-
generated = sdk.Secrets.generatePassword({
|
|
672
|
-
type: "Memorable",
|
|
673
|
-
parameters: {
|
|
674
|
-
separatorType: sdk.SeparatorType.Digits,
|
|
675
|
-
capitalize: args.capitalize !== false,
|
|
676
|
-
wordListType: sdk.WordListType.FullWords,
|
|
677
|
-
wordCount: args.wordCount || 5,
|
|
678
|
-
},
|
|
679
|
-
});
|
|
680
|
-
} else {
|
|
681
|
-
generated = sdk.Secrets.generatePassword({
|
|
682
|
-
type: "Random",
|
|
683
|
-
parameters: {
|
|
684
|
-
includeDigits: args.includeDigits !== false,
|
|
685
|
-
includeSymbols: args.includeSymbols !== false,
|
|
686
|
-
length: args.length || 20,
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
const pwd = typeof generated === "string" ? generated : generated.password || generated;
|
|
691
|
-
return { content: [{ type: "text", text: `Generated (${type}):\n${pwd}` }] };
|
|
692
|
-
}
|
|
884
|
+
async function createVaultHandler(args) {
|
|
885
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
886
|
+
const vault = await client.vaults.create({
|
|
887
|
+
title: args.title,
|
|
888
|
+
description: args.description || "",
|
|
889
|
+
});
|
|
890
|
+
return `Created vault **${vault.title}** (id: ${vault.id}).`;
|
|
891
|
+
}
|
|
693
892
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
isError: true,
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const category = sdk.ItemCategory[args.category || "Login"];
|
|
705
|
-
if (!category) {
|
|
706
|
-
return {
|
|
707
|
-
content: [{ type: "text", text: `Unknown category "${args.category}".` }],
|
|
708
|
-
isError: true,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
const fields = (args.fields || []).map((f) => {
|
|
712
|
-
const ft = sdk.ItemFieldType[f.type];
|
|
713
|
-
if (!ft) throw new Error(`Unknown field type "${f.type}"`);
|
|
714
|
-
const out = {
|
|
715
|
-
id: f.title.toLowerCase().replace(/\s+/g, "_"),
|
|
716
|
-
title: f.title,
|
|
717
|
-
fieldType: ft,
|
|
718
|
-
value: f.value,
|
|
719
|
-
};
|
|
720
|
-
if (f.sectionId) out.sectionId = f.sectionId;
|
|
721
|
-
return out;
|
|
722
|
-
});
|
|
723
|
-
if (args.notes) {
|
|
724
|
-
fields.push({
|
|
725
|
-
id: "notesPlain",
|
|
726
|
-
title: "notesPlain",
|
|
727
|
-
fieldType: sdk.ItemFieldType.Text,
|
|
728
|
-
value: args.notes,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
const item = await client.items.create({
|
|
732
|
-
title: args.title,
|
|
733
|
-
category,
|
|
734
|
-
vaultId: vault.id,
|
|
735
|
-
fields,
|
|
736
|
-
tags: args.tags || [],
|
|
737
|
-
websites: (args.websites || []).map((w) => ({
|
|
738
|
-
url: w.url,
|
|
739
|
-
label: w.label || "website",
|
|
740
|
-
autofillBehavior: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
741
|
-
})),
|
|
742
|
-
});
|
|
743
|
-
return {
|
|
744
|
-
content: [
|
|
745
|
-
{
|
|
746
|
-
type: "text",
|
|
747
|
-
text: `Created item **${item.title}** (id: ${item.id}) in vault "${vault.title}".`,
|
|
748
|
-
},
|
|
749
|
-
],
|
|
750
|
-
};
|
|
751
|
-
}
|
|
893
|
+
async function deleteVaultHandler(args) {
|
|
894
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
895
|
+
const vault = await resolveVault(client, args.vault);
|
|
896
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
897
|
+
await client.vaults.delete(vault.id);
|
|
898
|
+
return `Deleted vault "${vault.title}".`;
|
|
899
|
+
}
|
|
752
900
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
],
|
|
778
|
-
};
|
|
901
|
+
const HANDLERS = {
|
|
902
|
+
op_list_instances: listInstancesHandler,
|
|
903
|
+
op_add_instance: addInstanceHandler,
|
|
904
|
+
op_remove_instance: removeInstanceHandler,
|
|
905
|
+
op_list_vaults: listVaultsHandler,
|
|
906
|
+
op_list_items: listItemsHandler,
|
|
907
|
+
op_search_items: searchItemsHandler,
|
|
908
|
+
op_get_item: getItemHandler,
|
|
909
|
+
op_get_secret: getSecretHandler,
|
|
910
|
+
op_generate_password: generatePasswordHandler,
|
|
911
|
+
op_create_item: createItemHandler,
|
|
912
|
+
op_delete_item: deleteItemHandler,
|
|
913
|
+
op_create_vault: createVaultHandler,
|
|
914
|
+
op_delete_vault: deleteVaultHandler,
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// Drift guard: every declared tool must have a handler and vice versa. This is
|
|
918
|
+
// evaluated at module load so a missing/extra handler fails fast.
|
|
919
|
+
(function assertHandlerCompleteness() {
|
|
920
|
+
const toolNames = new Set(TOOL_DEFINITIONS.map((t) => t.name));
|
|
921
|
+
const handlerNames = new Set(Object.keys(HANDLERS));
|
|
922
|
+
for (const name of toolNames) {
|
|
923
|
+
if (!handlerNames.has(name)) {
|
|
924
|
+
throw new Error(`Tool "${name}" is declared but has no handler.`);
|
|
779
925
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const vault = await client.vaults.create({
|
|
785
|
-
title: args.title,
|
|
786
|
-
description: args.description || "",
|
|
787
|
-
});
|
|
788
|
-
return {
|
|
789
|
-
content: [
|
|
790
|
-
{ type: "text", text: `Created vault **${vault.title}** (id: ${vault.id}).` },
|
|
791
|
-
],
|
|
792
|
-
};
|
|
926
|
+
}
|
|
927
|
+
for (const name of handlerNames) {
|
|
928
|
+
if (!toolNames.has(name)) {
|
|
929
|
+
throw new Error(`Handler "${name}" exists but is not declared in TOOL_DEFINITIONS.`);
|
|
793
930
|
}
|
|
931
|
+
}
|
|
932
|
+
})();
|
|
794
933
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// MCP server wiring
|
|
936
|
+
// ============================================================================
|
|
937
|
+
|
|
938
|
+
const server = new Server(
|
|
939
|
+
{ name: INTEGRATION_NAME, version: PKG_VERSION },
|
|
940
|
+
{ capabilities: { tools: {} } },
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
944
|
+
tools: TOOL_DEFINITIONS,
|
|
945
|
+
}));
|
|
808
946
|
|
|
809
|
-
|
|
810
|
-
}
|
|
947
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
948
|
+
const { name, arguments: args } = request.params;
|
|
949
|
+
const handler = HANDLERS[name];
|
|
950
|
+
if (!handler) {
|
|
811
951
|
return {
|
|
812
|
-
content: [{ type: "text", text: `
|
|
952
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
953
|
+
isError: true,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const result = await handler(args || {});
|
|
958
|
+
return { content: [{ type: "text", text: String(result) }] };
|
|
959
|
+
} catch (e) {
|
|
960
|
+
return {
|
|
961
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
813
962
|
isError: true,
|
|
814
963
|
};
|
|
815
964
|
}
|
|
@@ -827,13 +976,35 @@ if (require.main === module) {
|
|
|
827
976
|
});
|
|
828
977
|
}
|
|
829
978
|
|
|
830
|
-
//
|
|
979
|
+
// ============================================================================
|
|
980
|
+
// Test exports
|
|
981
|
+
// ============================================================================
|
|
982
|
+
|
|
831
983
|
if (typeof module !== "undefined") {
|
|
832
984
|
module.exports = {
|
|
985
|
+
// config
|
|
986
|
+
CONFIG_DIR,
|
|
987
|
+
CONFIG_PATH,
|
|
833
988
|
loadConfigFile,
|
|
834
989
|
saveConfigFile,
|
|
990
|
+
normalizeInstance,
|
|
991
|
+
// instance resolution
|
|
835
992
|
getInstanceByName,
|
|
993
|
+
// helpers
|
|
994
|
+
collect,
|
|
995
|
+
resolveVault,
|
|
996
|
+
resolveItem,
|
|
836
997
|
redactField,
|
|
837
998
|
formatItem,
|
|
999
|
+
// handler map + tool list (for drift guard tests)
|
|
1000
|
+
TOOL_DEFINITIONS,
|
|
1001
|
+
HANDLERS,
|
|
1002
|
+
// direct handler access for unit tests that don't need the MCP transport
|
|
1003
|
+
listInstancesHandler,
|
|
1004
|
+
listVaultsHandler,
|
|
1005
|
+
listItemsHandler,
|
|
1006
|
+
searchItemsHandler,
|
|
1007
|
+
getItemHandler,
|
|
1008
|
+
generatePasswordHandler,
|
|
838
1009
|
};
|
|
839
1010
|
}
|