@rui.branco/1password-mcp 0.1.2 → 0.1.4
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/index.js +971 -670
- package/package.json +1 -1
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,60 +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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
86
112
|
|
|
87
|
-
const
|
|
88
|
-
|
|
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)) ||
|
|
89
122
|
instances[0] ||
|
|
90
123
|
null;
|
|
91
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
|
+
*/
|
|
92
129
|
function getInstanceByName(name) {
|
|
93
130
|
if (!name) return defaultInstance;
|
|
94
131
|
return instances.find((i) => i.name === name) || defaultInstance;
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
//
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// 1Password SDK client
|
|
136
|
+
// ============================================================================
|
|
98
137
|
|
|
99
|
-
// Cache SDK clients per instance
|
|
100
|
-
//
|
|
101
|
-
// 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.
|
|
102
140
|
const clientCache = new Map();
|
|
103
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
|
+
|
|
104
157
|
async function getClient(instance) {
|
|
105
158
|
if (!instance) {
|
|
106
159
|
throw new Error(
|
|
@@ -109,23 +162,8 @@ async function getClient(instance) {
|
|
|
109
162
|
}
|
|
110
163
|
if (clientCache.has(instance.name)) return clientCache.get(instance.name);
|
|
111
164
|
|
|
112
|
-
let auth;
|
|
113
|
-
if (instance.authType === "desktop") {
|
|
114
|
-
if (!instance.accountName) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`Instance "${instance.name}" is desktop-auth but has no accountName. Re-run setup and provide your 1Password account (e.g. "my.1password.com" or "my").`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
auth = new sdk.DesktopAuth(instance.accountName);
|
|
120
|
-
} else {
|
|
121
|
-
if (!instance.token) {
|
|
122
|
-
throw new Error(`Instance "${instance.name}" has no service account token.`);
|
|
123
|
-
}
|
|
124
|
-
auth = instance.token;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
165
|
const client = await sdk.createClient({
|
|
128
|
-
auth,
|
|
166
|
+
auth: buildAuth(instance),
|
|
129
167
|
integrationName: INTEGRATION_NAME,
|
|
130
168
|
integrationVersion: `v${PKG_VERSION}`,
|
|
131
169
|
});
|
|
@@ -137,37 +175,54 @@ function invalidateClient(name) {
|
|
|
137
175
|
clientCache.delete(name);
|
|
138
176
|
}
|
|
139
177
|
|
|
140
|
-
//
|
|
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
|
+
}
|
|
141
187
|
|
|
142
|
-
|
|
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
|
+
*/
|
|
143
192
|
async function resolveVault(client, vaultRef) {
|
|
144
193
|
if (!vaultRef) return null;
|
|
145
|
-
const vaults =
|
|
146
|
-
for await (const v of await client.vaults.list()) vaults.push(v);
|
|
194
|
+
const vaults = await collect(await client.vaults.list());
|
|
147
195
|
const byId = vaults.find((v) => v.id === vaultRef);
|
|
148
196
|
if (byId) return byId;
|
|
149
197
|
const lower = String(vaultRef).toLowerCase();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
return (
|
|
199
|
+
vaults.find((v) => (v.title || "").toLowerCase() === lower) ||
|
|
200
|
+
vaults.find((v) => (v.title || "").toLowerCase().startsWith(lower)) ||
|
|
201
|
+
null
|
|
202
|
+
);
|
|
155
203
|
}
|
|
156
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Same strategy as resolveVault but for items inside a specific vault.
|
|
207
|
+
*/
|
|
157
208
|
async function resolveItem(client, vault, itemRef) {
|
|
158
209
|
if (!itemRef) return null;
|
|
159
|
-
const items =
|
|
160
|
-
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));
|
|
161
211
|
const byId = items.find((i) => i.id === itemRef);
|
|
162
212
|
if (byId) return byId;
|
|
163
213
|
const lower = String(itemRef).toLowerCase();
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
return (
|
|
215
|
+
items.find((i) => (i.title || "").toLowerCase() === lower) ||
|
|
216
|
+
items.find((i) => (i.title || "").toLowerCase().startsWith(lower)) ||
|
|
217
|
+
null
|
|
218
|
+
);
|
|
169
219
|
}
|
|
170
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
|
+
*/
|
|
171
226
|
function redactField(field, includeConcealed) {
|
|
172
227
|
const out = {
|
|
173
228
|
id: field.id,
|
|
@@ -178,8 +233,13 @@ function redactField(field, includeConcealed) {
|
|
|
178
233
|
const isSecret = field.fieldType === "Concealed" || field.fieldType === "Totp";
|
|
179
234
|
if (!isSecret || includeConcealed) {
|
|
180
235
|
out.value = field.value;
|
|
181
|
-
if (
|
|
182
|
-
|
|
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;
|
|
183
243
|
}
|
|
184
244
|
} else {
|
|
185
245
|
out.value = "[REDACTED]";
|
|
@@ -187,6 +247,10 @@ function redactField(field, includeConcealed) {
|
|
|
187
247
|
return out;
|
|
188
248
|
}
|
|
189
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
|
+
*/
|
|
190
254
|
function formatItem(item, includeConcealed) {
|
|
191
255
|
return {
|
|
192
256
|
id: item.id,
|
|
@@ -194,682 +258,894 @@ function formatItem(item, includeConcealed) {
|
|
|
194
258
|
category: item.category,
|
|
195
259
|
vaultId: item.vaultId,
|
|
196
260
|
tags: item.tags || [],
|
|
197
|
-
websites: (item.websites || []).map((w) => ({
|
|
261
|
+
websites: (item.websites || []).map((w) => ({
|
|
262
|
+
url: w.url,
|
|
263
|
+
label: w.label,
|
|
264
|
+
})),
|
|
198
265
|
fields: (item.fields || []).map((f) => redactField(f, includeConcealed)),
|
|
199
266
|
sections: (item.sections || []).map((s) => ({ id: s.id, title: s.title })),
|
|
200
267
|
};
|
|
201
268
|
}
|
|
202
269
|
|
|
203
|
-
//
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Tool definitions (exposed via tools/list)
|
|
272
|
+
// ============================================================================
|
|
204
273
|
|
|
205
|
-
const
|
|
206
|
-
{
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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).",
|
|
313
|
+
},
|
|
218
314
|
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
},
|
|
245
|
-
required: ["name"],
|
|
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.",
|
|
246
340
|
},
|
|
247
341
|
},
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.",
|
|
257
356
|
},
|
|
258
357
|
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
358
|
+
required: ["vault"],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "op_search_items",
|
|
363
|
+
description:
|
|
364
|
+
"Search items by title across vaults. When the user asks for a specific credential by name (e.g. 'give me the wifi password', 'what's my GitHub login'), call this with `reveal: true` to get the password/credential values back in a single call — no follow-up op_get_item needed. If `vault` is omitted, searches every vault the account can see. When there are 1–5 matches with reveal=true, return all of them; the user already consented to seeing concealed values by asking for them.",
|
|
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, or 5 when reveal=true to avoid fetching too many items).",
|
|
379
|
+
},
|
|
380
|
+
reveal: {
|
|
381
|
+
type: "boolean",
|
|
382
|
+
description:
|
|
383
|
+
"If true, fetch each match's full item and return concealed field values (passwords, TOTPs) in cleartext. Use this when the user is asking for a credential by name — one call gets search + retrieval + reveal. Defaults to false.",
|
|
384
|
+
},
|
|
385
|
+
instance: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "Instance name. Uses default if omitted.",
|
|
268
388
|
},
|
|
269
389
|
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
390
|
+
required: ["query"],
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "op_get_item",
|
|
395
|
+
description:
|
|
396
|
+
"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.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
401
|
+
item: { type: "string", description: "Item id or title." },
|
|
402
|
+
includeConcealed: {
|
|
403
|
+
type: "boolean",
|
|
404
|
+
description: "Return concealed values in cleartext (default: false).",
|
|
405
|
+
},
|
|
406
|
+
instance: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "Instance name. Uses default if omitted.",
|
|
281
409
|
},
|
|
282
410
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
411
|
+
required: ["vault", "item"],
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "op_get_secret",
|
|
416
|
+
description:
|
|
417
|
+
"Resolve a 1Password secret reference (op://vault/item/field) and return the value.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
reference: {
|
|
422
|
+
type: "string",
|
|
423
|
+
description: "Secret reference like 'op://Private/GitHub/password'.",
|
|
424
|
+
},
|
|
425
|
+
instance: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Instance name. Uses default if omitted.",
|
|
296
428
|
},
|
|
297
429
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
430
|
+
required: ["reference"],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "op_generate_password",
|
|
435
|
+
description:
|
|
436
|
+
"Generate a password using 1Password's generator (Random / Memorable / Pin). Does not talk to the account.",
|
|
437
|
+
inputSchema: {
|
|
438
|
+
type: "object",
|
|
439
|
+
properties: {
|
|
440
|
+
type: {
|
|
441
|
+
type: "string",
|
|
442
|
+
description: "'Random' (default), 'Memorable', or 'Pin'.",
|
|
443
|
+
},
|
|
444
|
+
length: {
|
|
445
|
+
type: "number",
|
|
446
|
+
description: "Length for Random/Pin (default 20/8).",
|
|
447
|
+
},
|
|
448
|
+
includeDigits: {
|
|
449
|
+
type: "boolean",
|
|
450
|
+
description: "Random only (default true).",
|
|
451
|
+
},
|
|
452
|
+
includeSymbols: {
|
|
453
|
+
type: "boolean",
|
|
454
|
+
description: "Random only (default true).",
|
|
455
|
+
},
|
|
456
|
+
wordCount: {
|
|
457
|
+
type: "number",
|
|
458
|
+
description: "Memorable only (default 5).",
|
|
459
|
+
},
|
|
460
|
+
capitalize: {
|
|
461
|
+
type: "boolean",
|
|
462
|
+
description: "Memorable only (default true).",
|
|
314
463
|
},
|
|
315
464
|
},
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
465
|
+
required: [],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: "op_create_item",
|
|
470
|
+
description:
|
|
471
|
+
"Create a new item in a vault. Fields is an array of { title, type, value, sectionId? }. Type is Text | Concealed | Totp | Email | Url | Phone.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: {
|
|
475
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
476
|
+
title: { type: "string", description: "Item title." },
|
|
477
|
+
category: {
|
|
478
|
+
type: "string",
|
|
479
|
+
description:
|
|
480
|
+
"Item category (default: Login). One of: Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
|
|
481
|
+
},
|
|
482
|
+
fields: {
|
|
483
|
+
type: "array",
|
|
484
|
+
description:
|
|
485
|
+
"Field definitions: [{ title, type, value, sectionId? }].",
|
|
486
|
+
items: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: {
|
|
489
|
+
title: { type: "string" },
|
|
490
|
+
type: { type: "string" },
|
|
491
|
+
value: { type: "string" },
|
|
492
|
+
sectionId: { type: "string" },
|
|
326
493
|
},
|
|
327
|
-
|
|
494
|
+
required: ["title", "type", "value"],
|
|
328
495
|
},
|
|
329
|
-
required: ["reference"],
|
|
330
496
|
},
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
type: {
|
|
340
|
-
type: "string",
|
|
341
|
-
description: "'Random' (default), 'Memorable', or 'Pin'.",
|
|
497
|
+
tags: { type: "array", items: { type: "string" } },
|
|
498
|
+
websites: {
|
|
499
|
+
type: "array",
|
|
500
|
+
items: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
url: { type: "string" },
|
|
504
|
+
label: { type: "string" },
|
|
342
505
|
},
|
|
343
|
-
|
|
344
|
-
includeDigits: { type: "boolean", description: "Random only (default true)." },
|
|
345
|
-
includeSymbols: { type: "boolean", description: "Random only (default true)." },
|
|
346
|
-
wordCount: { type: "number", description: "Memorable only (default 5)." },
|
|
347
|
-
capitalize: { type: "boolean", description: "Memorable only (default true)." },
|
|
506
|
+
required: ["url"],
|
|
348
507
|
},
|
|
349
|
-
|
|
508
|
+
},
|
|
509
|
+
notes: {
|
|
510
|
+
type: "string",
|
|
511
|
+
description:
|
|
512
|
+
"Optional notes (stored as a Text field called 'notesPlain').",
|
|
513
|
+
},
|
|
514
|
+
instance: {
|
|
515
|
+
type: "string",
|
|
516
|
+
description: "Instance name. Uses default if omitted.",
|
|
350
517
|
},
|
|
351
518
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
websites: {
|
|
382
|
-
type: "array",
|
|
383
|
-
items: {
|
|
384
|
-
type: "object",
|
|
385
|
-
properties: {
|
|
386
|
-
url: { type: "string" },
|
|
387
|
-
label: { type: "string" },
|
|
388
|
-
},
|
|
389
|
-
required: ["url"],
|
|
390
|
-
},
|
|
519
|
+
required: ["vault", "title"],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: "op_update_item",
|
|
524
|
+
description:
|
|
525
|
+
"Update an existing item. Patch fields by title — e.g. to change the password on 'GitHub', pass fields: [{title: 'password', value: 'new-pwd'}]. To add a new field, include its type. Omitting fields leaves them untouched. Can also rename the item (title) or replace its tags.",
|
|
526
|
+
inputSchema: {
|
|
527
|
+
type: "object",
|
|
528
|
+
properties: {
|
|
529
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
530
|
+
item: { type: "string", description: "Item id or title to update." },
|
|
531
|
+
title: { type: "string", description: "New title (optional)." },
|
|
532
|
+
tags: {
|
|
533
|
+
type: "array",
|
|
534
|
+
items: { type: "string" },
|
|
535
|
+
description: "Replace the item's tag list (optional).",
|
|
536
|
+
},
|
|
537
|
+
fields: {
|
|
538
|
+
type: "array",
|
|
539
|
+
description:
|
|
540
|
+
"Field patches: [{title, value, type?, sectionId?}]. Existing fields are matched by title (case-insensitive) and their values replaced. New fields require `type`.",
|
|
541
|
+
items: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
title: { type: "string" },
|
|
545
|
+
value: { type: "string" },
|
|
546
|
+
type: { type: "string" },
|
|
547
|
+
sectionId: { type: "string" },
|
|
391
548
|
},
|
|
392
|
-
|
|
393
|
-
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
549
|
+
required: ["title", "value"],
|
|
394
550
|
},
|
|
395
|
-
|
|
551
|
+
},
|
|
552
|
+
instance: {
|
|
553
|
+
type: "string",
|
|
554
|
+
description: "Instance name. Uses default if omitted.",
|
|
396
555
|
},
|
|
397
556
|
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
557
|
+
required: ["vault", "item"],
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "op_update_vault",
|
|
562
|
+
description:
|
|
563
|
+
"Update a vault's title and/or description. Pass at least one of them.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
568
|
+
title: { type: "string", description: "New title (optional)." },
|
|
569
|
+
description: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "New description (optional).",
|
|
572
|
+
},
|
|
573
|
+
instance: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description: "Instance name. Uses default if omitted.",
|
|
409
576
|
},
|
|
410
577
|
},
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
578
|
+
required: ["vault"],
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: "op_delete_item",
|
|
583
|
+
description: "Delete an item by vault + item reference.",
|
|
584
|
+
inputSchema: {
|
|
585
|
+
type: "object",
|
|
586
|
+
properties: {
|
|
587
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
588
|
+
item: { type: "string", description: "Item id or title." },
|
|
589
|
+
instance: {
|
|
590
|
+
type: "string",
|
|
591
|
+
description: "Instance name. Uses default if omitted.",
|
|
422
592
|
},
|
|
423
593
|
},
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
594
|
+
required: ["vault", "item"],
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "op_create_vault",
|
|
599
|
+
description: "Create a new vault.",
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: "object",
|
|
602
|
+
properties: {
|
|
603
|
+
title: { type: "string", description: "Vault title." },
|
|
604
|
+
description: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: "Optional description.",
|
|
607
|
+
},
|
|
608
|
+
instance: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "Instance name. Uses default if omitted.",
|
|
434
611
|
},
|
|
435
612
|
},
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
613
|
+
required: ["title"],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "op_delete_vault",
|
|
618
|
+
description: "Delete a vault by id or title.",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
type: "object",
|
|
621
|
+
properties: {
|
|
622
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
623
|
+
instance: {
|
|
624
|
+
type: "string",
|
|
625
|
+
description: "Instance name. Uses default if omitted.",
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
required: ["vault"],
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
];
|
|
439
632
|
|
|
440
|
-
|
|
441
|
-
|
|
633
|
+
// ============================================================================
|
|
634
|
+
// Handlers
|
|
635
|
+
//
|
|
636
|
+
// Each handler is keyed by tool name and receives the tool arguments. It
|
|
637
|
+
// should return a plain string (rendered as text content by the dispatcher)
|
|
638
|
+
// or throw on failure. The dispatcher handles MCP wrapping and error shape.
|
|
639
|
+
// ============================================================================
|
|
442
640
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
text: "No instances configured. Run `1password-mcp setup` or call op_add_instance.",
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
const def = defaultInstance?.name;
|
|
456
|
-
let text = `# 1Password Instances (${instances.length})\n\n`;
|
|
457
|
-
for (const inst of instances) {
|
|
458
|
-
const isDefault = inst.name === def ? " **(default)**" : "";
|
|
459
|
-
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
460
|
-
const auth =
|
|
461
|
-
inst.authType === "desktop"
|
|
462
|
-
? ` (desktop: ${inst.accountName})`
|
|
463
|
-
: " (serviceAccount)";
|
|
464
|
-
text += `- **${inst.name}**${isDefault}${auth}${desc}\n`;
|
|
465
|
-
}
|
|
466
|
-
return { content: [{ type: "text", text }] };
|
|
641
|
+
async function addInstanceHandler(args) {
|
|
642
|
+
const instName = args.name.trim();
|
|
643
|
+
const authType = args.authType || (args.token ? "serviceAccount" : "desktop");
|
|
644
|
+
|
|
645
|
+
if (authType === "serviceAccount") {
|
|
646
|
+
if (!args.token) {
|
|
647
|
+
throw new Error("serviceAccount authType requires a token.");
|
|
467
648
|
}
|
|
649
|
+
if (!/^ops_[A-Za-z0-9_\-]+/.test(args.token)) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
"Token does not look like a 1Password service account token (expected prefix 'ops_').",
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
} else if (authType === "desktop") {
|
|
655
|
+
if (!args.accountName) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
"desktop authType requires accountName (e.g. 'my.1password.com').",
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
throw new Error(`Unknown authType "${authType}".`);
|
|
662
|
+
}
|
|
468
663
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
content: [
|
|
483
|
-
{
|
|
484
|
-
type: "text",
|
|
485
|
-
text: "Token does not look like a 1Password service account token (expected prefix 'ops_').",
|
|
486
|
-
},
|
|
487
|
-
],
|
|
488
|
-
isError: true,
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
} else if (authType === "desktop") {
|
|
492
|
-
if (!args.accountName) {
|
|
493
|
-
return {
|
|
494
|
-
content: [
|
|
495
|
-
{
|
|
496
|
-
type: "text",
|
|
497
|
-
text: "desktop authType requires accountName (e.g. 'my.1password.com').",
|
|
498
|
-
},
|
|
499
|
-
],
|
|
500
|
-
isError: true,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
} else {
|
|
504
|
-
return {
|
|
505
|
-
content: [{ type: "text", text: `Unknown authType "${authType}".` }],
|
|
506
|
-
isError: true,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
664
|
+
// Validate by creating a client and listing one vault.
|
|
665
|
+
const probeAuth =
|
|
666
|
+
authType === "desktop" ? new sdk.DesktopAuth(args.accountName) : args.token;
|
|
667
|
+
const tempClient = await sdk.createClient({
|
|
668
|
+
auth: probeAuth,
|
|
669
|
+
integrationName: INTEGRATION_NAME,
|
|
670
|
+
integrationVersion: `v${PKG_VERSION}`,
|
|
671
|
+
});
|
|
672
|
+
const probe = [];
|
|
673
|
+
for await (const v of await tempClient.vaults.list()) {
|
|
674
|
+
probe.push(v);
|
|
675
|
+
if (probe.length >= 1) break;
|
|
676
|
+
}
|
|
509
677
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const probe = [];
|
|
522
|
-
for await (const v of await tempClient.vaults.list()) {
|
|
523
|
-
probe.push(v);
|
|
524
|
-
if (probe.length >= 1) break;
|
|
525
|
-
}
|
|
526
|
-
} catch (e) {
|
|
527
|
-
return {
|
|
528
|
-
content: [{ type: "text", text: `Auth validation failed: ${e.message}` }],
|
|
529
|
-
isError: true,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
678
|
+
const newInst = {
|
|
679
|
+
name: instName,
|
|
680
|
+
authType,
|
|
681
|
+
token: authType === "serviceAccount" ? args.token : undefined,
|
|
682
|
+
accountName: authType === "desktop" ? args.accountName : undefined,
|
|
683
|
+
description: args.description || "",
|
|
684
|
+
};
|
|
685
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
686
|
+
if (existingIdx >= 0) instances[existingIdx] = newInst;
|
|
687
|
+
else instances.push(newInst);
|
|
688
|
+
invalidateClient(instName);
|
|
532
689
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
548
|
-
const toSave = { name: instName, authType };
|
|
549
|
-
if (authType === "serviceAccount") toSave.token = args.token;
|
|
550
|
-
if (authType === "desktop") toSave.accountName = args.accountName;
|
|
551
|
-
if (args.description) toSave.description = args.description;
|
|
552
|
-
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
553
|
-
else savedConfig.instances.push(toSave);
|
|
554
|
-
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
555
|
-
savedConfig.defaultInstance = instName;
|
|
556
|
-
}
|
|
557
|
-
saveConfigFile(savedConfig);
|
|
690
|
+
const savedConfig = loadConfigFile();
|
|
691
|
+
if (!savedConfig.instances) savedConfig.instances = [];
|
|
692
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
693
|
+
const toSave = { name: instName, authType };
|
|
694
|
+
if (authType === "serviceAccount") toSave.token = args.token;
|
|
695
|
+
if (authType === "desktop") toSave.accountName = args.accountName;
|
|
696
|
+
if (args.description) toSave.description = args.description;
|
|
697
|
+
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
698
|
+
else savedConfig.instances.push(toSave);
|
|
699
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
700
|
+
savedConfig.defaultInstance = instName;
|
|
701
|
+
defaultInstance = newInst;
|
|
702
|
+
}
|
|
703
|
+
saveConfigFile(savedConfig);
|
|
558
704
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
text: `${existingIdx >= 0 ? "Updated" : "Added"} instance "${instName}" (${authType}).${args.setDefault ? " Set as default." : ""}`,
|
|
564
|
-
},
|
|
565
|
-
],
|
|
566
|
-
};
|
|
567
|
-
}
|
|
705
|
+
const verb = existingIdx >= 0 ? "Updated" : "Added";
|
|
706
|
+
const suffix = args.setDefault ? " Set as default." : "";
|
|
707
|
+
return `${verb} instance "${instName}" (${authType}).${suffix}`;
|
|
708
|
+
}
|
|
568
709
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
const idx = instances.findIndex((i) => i.name === args.name);
|
|
577
|
-
if (idx < 0) {
|
|
578
|
-
return {
|
|
579
|
-
content: [{ type: "text", text: `Instance "${args.name}" not found.` }],
|
|
580
|
-
isError: true,
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
instances.splice(idx, 1);
|
|
584
|
-
invalidateClient(args.name);
|
|
585
|
-
const savedConfig = loadConfigFile();
|
|
586
|
-
if (savedConfig.instances) {
|
|
587
|
-
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== args.name);
|
|
588
|
-
if (savedConfig.defaultInstance === args.name) {
|
|
589
|
-
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
590
|
-
}
|
|
591
|
-
saveConfigFile(savedConfig);
|
|
592
|
-
}
|
|
593
|
-
return { content: [{ type: "text", text: `Removed instance "${args.name}".` }] };
|
|
594
|
-
}
|
|
710
|
+
async function removeInstanceHandler(args) {
|
|
711
|
+
if (instances.length <= 1) {
|
|
712
|
+
throw new Error("Cannot remove the last remaining instance.");
|
|
713
|
+
}
|
|
714
|
+
const idx = instances.findIndex((i) => i.name === args.name);
|
|
715
|
+
if (idx < 0) throw new Error(`Instance "${args.name}" not found.`);
|
|
595
716
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const client = await getClient(inst);
|
|
599
|
-
const vaults = [];
|
|
600
|
-
for await (const v of await client.vaults.list()) vaults.push(v);
|
|
601
|
-
if (vaults.length === 0) {
|
|
602
|
-
return { content: [{ type: "text", text: "No vaults visible to this service account." }] };
|
|
603
|
-
}
|
|
604
|
-
let text = `# Vaults (${vaults.length})\n\n`;
|
|
605
|
-
for (const v of vaults) text += `- **${v.title}** (id: ${v.id})\n`;
|
|
606
|
-
return { content: [{ type: "text", text }] };
|
|
607
|
-
}
|
|
717
|
+
instances.splice(idx, 1);
|
|
718
|
+
invalidateClient(args.name);
|
|
608
719
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
const items = [];
|
|
620
|
-
for await (const it of await client.items.list(vault.id)) items.push(it);
|
|
621
|
-
if (items.length === 0) {
|
|
622
|
-
return { content: [{ type: "text", text: `No items in vault "${vault.title}".` }] };
|
|
623
|
-
}
|
|
624
|
-
let text = `# Items in ${vault.title} (${items.length})\n\n`;
|
|
625
|
-
for (const it of items) {
|
|
626
|
-
text += `- **${it.title}** (id: ${it.id}, category: ${it.category})\n`;
|
|
627
|
-
}
|
|
628
|
-
return { content: [{ type: "text", text }] };
|
|
720
|
+
const savedConfig = loadConfigFile();
|
|
721
|
+
if (savedConfig.instances) {
|
|
722
|
+
savedConfig.instances = savedConfig.instances.filter(
|
|
723
|
+
(i) => i.name !== args.name,
|
|
724
|
+
);
|
|
725
|
+
if (savedConfig.defaultInstance === args.name) {
|
|
726
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
727
|
+
defaultInstance = instances[0] || null;
|
|
629
728
|
}
|
|
729
|
+
saveConfigFile(savedConfig);
|
|
730
|
+
}
|
|
731
|
+
return `Removed instance "${args.name}".`;
|
|
732
|
+
}
|
|
630
733
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
} else {
|
|
648
|
-
for await (const v of await client.vaults.list()) targetVaults.push(v);
|
|
649
|
-
}
|
|
734
|
+
function listInstancesHandler() {
|
|
735
|
+
if (instances.length === 0) {
|
|
736
|
+
return "No instances configured. Run `1password-mcp setup` or call op_add_instance.";
|
|
737
|
+
}
|
|
738
|
+
const def = defaultInstance?.name;
|
|
739
|
+
const lines = instances.map((inst) => {
|
|
740
|
+
const isDefault = inst.name === def ? " **(default)**" : "";
|
|
741
|
+
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
742
|
+
const auth =
|
|
743
|
+
inst.authType === "desktop"
|
|
744
|
+
? ` (desktop: ${inst.accountName})`
|
|
745
|
+
: " (serviceAccount)";
|
|
746
|
+
return `- **${inst.name}**${isDefault}${auth}${desc}`;
|
|
747
|
+
});
|
|
748
|
+
return `# 1Password Instances (${instances.length})\n\n${lines.join("\n")}`;
|
|
749
|
+
}
|
|
650
750
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
751
|
+
async function listVaultsHandler(args) {
|
|
752
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
753
|
+
const vaults = await collect(await client.vaults.list());
|
|
754
|
+
if (vaults.length === 0) {
|
|
755
|
+
return "No vaults visible to this instance.";
|
|
756
|
+
}
|
|
757
|
+
const lines = vaults.map((v) => `- **${v.title}** (id: ${v.id})`);
|
|
758
|
+
return `# Vaults (${vaults.length})\n\n${lines.join("\n")}`;
|
|
759
|
+
}
|
|
661
760
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
761
|
+
async function listItemsHandler(args) {
|
|
762
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
763
|
+
const vault = await resolveVault(client, args.vault);
|
|
764
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
765
|
+
|
|
766
|
+
const items = await collect(await client.items.list(vault.id));
|
|
767
|
+
if (items.length === 0) {
|
|
768
|
+
return `No items in vault "${vault.title}".`;
|
|
769
|
+
}
|
|
770
|
+
const lines = items.map(
|
|
771
|
+
(i) => `- **${i.title}** (id: ${i.id}, category: ${i.category})`,
|
|
772
|
+
);
|
|
773
|
+
return `# Items in ${vault.title} (${items.length})\n\n${lines.join("\n")}`;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Core search implementation — pulled out of the handler so it can be unit
|
|
778
|
+
* tested with a fake client. Accepts an already-resolved client.
|
|
779
|
+
*/
|
|
780
|
+
async function searchItems(client, { query, vault, reveal, limit }) {
|
|
781
|
+
const wantReveal = !!reveal;
|
|
782
|
+
const max = limit || (wantReveal ? 5 : 25);
|
|
783
|
+
const q = String(query).toLowerCase();
|
|
784
|
+
|
|
785
|
+
let targetVaults;
|
|
786
|
+
if (vault) {
|
|
787
|
+
const v = await resolveVault(client, vault);
|
|
788
|
+
if (!v) throw new Error(`Vault "${vault}" not found.`);
|
|
789
|
+
targetVaults = [v];
|
|
790
|
+
} else {
|
|
791
|
+
targetVaults = await collect(await client.vaults.list());
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const hits = [];
|
|
795
|
+
for (const v of targetVaults) {
|
|
796
|
+
for await (const it of await client.items.list(v.id)) {
|
|
797
|
+
if ((it.title || "").toLowerCase().includes(q)) {
|
|
798
|
+
hits.push({ ...it, _vaultId: v.id, _vaultTitle: v.title });
|
|
799
|
+
if (hits.length >= max) break;
|
|
670
800
|
}
|
|
671
|
-
return { content: [{ type: "text", text }] };
|
|
672
801
|
}
|
|
802
|
+
if (hits.length >= max) break;
|
|
803
|
+
}
|
|
673
804
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
text += `- **Vault:** ${vault.title} (${formatted.vaultId})\n`;
|
|
695
|
-
text += `- **Item ID:** ${formatted.id}\n`;
|
|
696
|
-
if (formatted.tags.length > 0) text += `- **Tags:** ${formatted.tags.join(", ")}\n`;
|
|
697
|
-
if (formatted.websites.length > 0) {
|
|
698
|
-
text += `- **Websites:** ${formatted.websites.map((w) => w.url).join(", ")}\n`;
|
|
699
|
-
}
|
|
700
|
-
if (formatted.fields.length > 0) {
|
|
701
|
-
text += `\n## Fields\n\n`;
|
|
702
|
-
for (const f of formatted.fields) {
|
|
703
|
-
const val = f.value === undefined ? "" : f.value;
|
|
704
|
-
text += `- **${f.title}** (${f.type}): ${val}${f.otp ? ` (OTP now: ${f.otp})` : ""}\n`;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (!args.includeConcealed) {
|
|
708
|
-
text += `\n_Concealed fields are redacted. Call with includeConcealed=true to reveal._`;
|
|
805
|
+
if (hits.length === 0) return `No items matched "${query}".`;
|
|
806
|
+
|
|
807
|
+
if (!wantReveal) {
|
|
808
|
+
const lines = hits.map(
|
|
809
|
+
(it) =>
|
|
810
|
+
`- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})`,
|
|
811
|
+
);
|
|
812
|
+
return `# Search: "${query}" (${hits.length})\n\n${lines.join("\n")}`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// reveal=true: fetch each hit's full item in parallel and render every
|
|
816
|
+
// field (including concealed values). One-shot path for "give me the X
|
|
817
|
+
// credential" style queries.
|
|
818
|
+
const full = await Promise.all(
|
|
819
|
+
hits.map(async (it) => {
|
|
820
|
+
try {
|
|
821
|
+
const item = await client.items.get(it._vaultId, it.id);
|
|
822
|
+
return { item, _vaultTitle: it._vaultTitle, _error: null };
|
|
823
|
+
} catch (e) {
|
|
824
|
+
return { item: it, _vaultTitle: it._vaultTitle, _error: e.message };
|
|
709
825
|
}
|
|
710
|
-
|
|
711
|
-
|
|
826
|
+
}),
|
|
827
|
+
);
|
|
712
828
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const value = await client.secrets.resolve(args.reference);
|
|
717
|
-
return {
|
|
718
|
-
content: [{ type: "text", text: `Secret for ${args.reference}:\n${value}` }],
|
|
719
|
-
};
|
|
829
|
+
const sections = full.map((entry) => {
|
|
830
|
+
if (entry._error) {
|
|
831
|
+
return `## ${entry.item.title}\n\n_Failed to fetch: ${entry._error}_`;
|
|
720
832
|
}
|
|
833
|
+
const it = entry.item;
|
|
834
|
+
const fields = (it.fields || [])
|
|
835
|
+
.map((f) => redactField(f, true))
|
|
836
|
+
.filter((f) => f.value !== undefined && f.value !== "");
|
|
837
|
+
const fieldLines = fields.length
|
|
838
|
+
? fields
|
|
839
|
+
.map((f) => {
|
|
840
|
+
const otp = f.otp ? ` (OTP now: ${f.otp})` : "";
|
|
841
|
+
return `- **${f.title}** (${f.type}): ${f.value}${otp}`;
|
|
842
|
+
})
|
|
843
|
+
.join("\n")
|
|
844
|
+
: "_(no fields)_";
|
|
845
|
+
const websites = (it.websites || []).map((w) => w.url).filter(Boolean);
|
|
846
|
+
const websiteLine = websites.length
|
|
847
|
+
? `- **URLs:** ${websites.join(", ")}\n`
|
|
848
|
+
: "";
|
|
849
|
+
return `## ${it.title} (${it.category})\n\n- **Vault:** ${entry._vaultTitle}\n- **Item ID:** ${it.id}\n${websiteLine}\n${fieldLines}`;
|
|
850
|
+
});
|
|
721
851
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
852
|
+
return `# Search (revealed): "${query}" (${hits.length})\n\n${sections.join("\n\n")}`;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function searchItemsHandler(args) {
|
|
856
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
857
|
+
return searchItems(client, args);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function getItemHandler(args) {
|
|
861
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
862
|
+
const vault = await resolveVault(client, args.vault);
|
|
863
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
864
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
865
|
+
if (!itemRef) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const full = await client.items.get(vault.id, itemRef.id);
|
|
871
|
+
const formatted = formatItem(full, !!args.includeConcealed);
|
|
872
|
+
|
|
873
|
+
const lines = [
|
|
874
|
+
`# ${formatted.title} (${formatted.category})`,
|
|
875
|
+
"",
|
|
876
|
+
`- **Vault:** ${vault.title} (${formatted.vaultId})`,
|
|
877
|
+
`- **Item ID:** ${formatted.id}`,
|
|
878
|
+
];
|
|
879
|
+
if (formatted.tags.length) lines.push(`- **Tags:** ${formatted.tags.join(", ")}`);
|
|
880
|
+
if (formatted.websites.length) {
|
|
881
|
+
lines.push(
|
|
882
|
+
`- **Websites:** ${formatted.websites.map((w) => w.url).join(", ")}`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (formatted.fields.length) {
|
|
886
|
+
lines.push("", "## Fields", "");
|
|
887
|
+
for (const f of formatted.fields) {
|
|
888
|
+
const val = f.value === undefined ? "" : f.value;
|
|
889
|
+
const otp = f.otp ? ` (OTP now: ${f.otp})` : "";
|
|
890
|
+
lines.push(`- **${f.title}** (${f.type}): ${val}${otp}`);
|
|
752
891
|
}
|
|
892
|
+
}
|
|
893
|
+
if (!args.includeConcealed) {
|
|
894
|
+
lines.push(
|
|
895
|
+
"",
|
|
896
|
+
"_Concealed fields are redacted. Call with includeConcealed=true to reveal._",
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
return lines.join("\n");
|
|
900
|
+
}
|
|
753
901
|
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
902
|
+
async function getSecretHandler(args) {
|
|
903
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
904
|
+
const value = await client.secrets.resolve(args.reference);
|
|
905
|
+
return `Secret for ${args.reference}:\n${value}`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function generatePasswordHandler(args) {
|
|
909
|
+
const type = args.type || "Random";
|
|
910
|
+
let generated;
|
|
911
|
+
if (type === "Pin") {
|
|
912
|
+
generated = sdk.Secrets.generatePassword({
|
|
913
|
+
type: "Pin",
|
|
914
|
+
parameters: { length: args.length || 8 },
|
|
915
|
+
});
|
|
916
|
+
} else if (type === "Memorable") {
|
|
917
|
+
generated = sdk.Secrets.generatePassword({
|
|
918
|
+
type: "Memorable",
|
|
919
|
+
parameters: {
|
|
920
|
+
separatorType: sdk.SeparatorType.Digits,
|
|
921
|
+
capitalize: args.capitalize !== false,
|
|
922
|
+
wordListType: sdk.WordListType.FullWords,
|
|
923
|
+
wordCount: args.wordCount || 5,
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
} else {
|
|
927
|
+
generated = sdk.Secrets.generatePassword({
|
|
928
|
+
type: "Random",
|
|
929
|
+
parameters: {
|
|
930
|
+
includeDigits: args.includeDigits !== false,
|
|
931
|
+
includeSymbols: args.includeSymbols !== false,
|
|
932
|
+
length: args.length || 20,
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const pwd =
|
|
937
|
+
typeof generated === "string" ? generated : generated.password || generated;
|
|
938
|
+
return `Generated (${type}):\n${pwd}`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function updateItemHandler(args) {
|
|
942
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
943
|
+
const vault = await resolveVault(client, args.vault);
|
|
944
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
945
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
946
|
+
if (!itemRef) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const current = await client.items.get(vault.id, itemRef.id);
|
|
953
|
+
const updated = { ...current };
|
|
954
|
+
|
|
955
|
+
if (args.title) updated.title = args.title;
|
|
956
|
+
if (args.tags) updated.tags = args.tags;
|
|
957
|
+
|
|
958
|
+
if (Array.isArray(args.fields) && args.fields.length > 0) {
|
|
959
|
+
const existing = current.fields || [];
|
|
960
|
+
const patchesByTitle = new Map(
|
|
961
|
+
args.fields.map((p) => [p.title.toLowerCase(), p]),
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// Patch existing fields in place, preserving id/type/sectionId
|
|
965
|
+
const patchedExisting = existing.map((f) => {
|
|
966
|
+
const patch = patchesByTitle.get((f.title || "").toLowerCase());
|
|
967
|
+
if (!patch) return f;
|
|
968
|
+
patchesByTitle.delete((f.title || "").toLowerCase());
|
|
969
|
+
return { ...f, value: patch.value };
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Remaining patches = new fields to add (must specify type)
|
|
973
|
+
const additions = [];
|
|
974
|
+
for (const patch of patchesByTitle.values()) {
|
|
975
|
+
if (!patch.type) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`Field "${patch.title}" doesn't exist on the item — pass a 'type' to add it as a new field.`,
|
|
978
|
+
);
|
|
790
979
|
}
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
label: w.label || "website",
|
|
800
|
-
autofillBehavior: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
801
|
-
})),
|
|
980
|
+
const ft = sdk.ItemFieldType[patch.type];
|
|
981
|
+
if (!ft) throw new Error(`Unknown field type "${patch.type}".`);
|
|
982
|
+
additions.push({
|
|
983
|
+
id: patch.title.toLowerCase().replace(/\s+/g, "_"),
|
|
984
|
+
title: patch.title,
|
|
985
|
+
fieldType: ft,
|
|
986
|
+
value: patch.value,
|
|
987
|
+
sectionId: patch.sectionId,
|
|
802
988
|
});
|
|
803
|
-
return {
|
|
804
|
-
content: [
|
|
805
|
-
{
|
|
806
|
-
type: "text",
|
|
807
|
-
text: `Created item **${item.title}** (id: ${item.id}) in vault "${vault.title}".`,
|
|
808
|
-
},
|
|
809
|
-
],
|
|
810
|
-
};
|
|
811
989
|
}
|
|
990
|
+
updated.fields = [...patchedExisting, ...additions];
|
|
991
|
+
}
|
|
812
992
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const vault = await resolveVault(client, args.vault);
|
|
817
|
-
if (!vault) {
|
|
818
|
-
return {
|
|
819
|
-
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
820
|
-
isError: true,
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
const itemRef = await resolveItem(client, vault, args.item);
|
|
824
|
-
if (!itemRef) {
|
|
825
|
-
return {
|
|
826
|
-
content: [{ type: "text", text: `Item "${args.item}" not found in vault "${vault.title}".` }],
|
|
827
|
-
isError: true,
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
await client.items.delete(vault.id, itemRef.id);
|
|
831
|
-
return {
|
|
832
|
-
content: [
|
|
833
|
-
{
|
|
834
|
-
type: "text",
|
|
835
|
-
text: `Deleted item "${itemRef.title}" from vault "${vault.title}".`,
|
|
836
|
-
},
|
|
837
|
-
],
|
|
838
|
-
};
|
|
839
|
-
}
|
|
993
|
+
const result = await client.items.put(updated);
|
|
994
|
+
return `Updated item "${result.title}" (id: ${result.id}) in vault "${vault.title}".`;
|
|
995
|
+
}
|
|
840
996
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
997
|
+
async function updateVaultHandler(args) {
|
|
998
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
999
|
+
const vault = await resolveVault(client, args.vault);
|
|
1000
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
1001
|
+
if (!args.title && args.description === undefined) {
|
|
1002
|
+
throw new Error("Provide at least one of title or description.");
|
|
1003
|
+
}
|
|
1004
|
+
await client.vaults.update(vault.id, {
|
|
1005
|
+
title: args.title || vault.title,
|
|
1006
|
+
description: args.description !== undefined ? args.description : vault.description || "",
|
|
1007
|
+
});
|
|
1008
|
+
return `Updated vault "${args.title || vault.title}" (id: ${vault.id}).`;
|
|
1009
|
+
}
|
|
854
1010
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1011
|
+
async function createItemHandler(args) {
|
|
1012
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
1013
|
+
const vault = await resolveVault(client, args.vault);
|
|
1014
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
1015
|
+
|
|
1016
|
+
const category = sdk.ItemCategory[args.category || "Login"];
|
|
1017
|
+
if (!category) throw new Error(`Unknown category "${args.category}".`);
|
|
1018
|
+
|
|
1019
|
+
const fields = (args.fields || []).map((f) => {
|
|
1020
|
+
const ft = sdk.ItemFieldType[f.type];
|
|
1021
|
+
if (!ft) throw new Error(`Unknown field type "${f.type}"`);
|
|
1022
|
+
const out = {
|
|
1023
|
+
id: f.title.toLowerCase().replace(/\s+/g, "_"),
|
|
1024
|
+
title: f.title,
|
|
1025
|
+
fieldType: ft,
|
|
1026
|
+
value: f.value,
|
|
1027
|
+
};
|
|
1028
|
+
if (f.sectionId) out.sectionId = f.sectionId;
|
|
1029
|
+
return out;
|
|
1030
|
+
});
|
|
1031
|
+
if (args.notes) {
|
|
1032
|
+
fields.push({
|
|
1033
|
+
id: "notesPlain",
|
|
1034
|
+
title: "notesPlain",
|
|
1035
|
+
fieldType: sdk.ItemFieldType.Text,
|
|
1036
|
+
value: args.notes,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const item = await client.items.create({
|
|
1041
|
+
title: args.title,
|
|
1042
|
+
category,
|
|
1043
|
+
vaultId: vault.id,
|
|
1044
|
+
fields,
|
|
1045
|
+
tags: args.tags || [],
|
|
1046
|
+
websites: (args.websites || []).map((w) => ({
|
|
1047
|
+
url: w.url,
|
|
1048
|
+
label: w.label || "website",
|
|
1049
|
+
autofillBehavior: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
1050
|
+
})),
|
|
1051
|
+
});
|
|
1052
|
+
return `Created item **${item.title}** (id: ${item.id}) in vault "${vault.title}".`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function deleteItemHandler(args) {
|
|
1056
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
1057
|
+
const vault = await resolveVault(client, args.vault);
|
|
1058
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
1059
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
1060
|
+
if (!itemRef) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
await client.items.delete(vault.id, itemRef.id);
|
|
1066
|
+
return `Deleted item "${itemRef.title}" from vault "${vault.title}".`;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function createVaultHandler(args) {
|
|
1070
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
1071
|
+
const vault = await client.vaults.create({
|
|
1072
|
+
title: args.title,
|
|
1073
|
+
description: args.description || "",
|
|
1074
|
+
});
|
|
1075
|
+
return `Created vault **${vault.title}** (id: ${vault.id}).`;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function deleteVaultHandler(args) {
|
|
1079
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
1080
|
+
const vault = await resolveVault(client, args.vault);
|
|
1081
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
1082
|
+
await client.vaults.delete(vault.id);
|
|
1083
|
+
return `Deleted vault "${vault.title}".`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const HANDLERS = {
|
|
1087
|
+
op_list_instances: listInstancesHandler,
|
|
1088
|
+
op_add_instance: addInstanceHandler,
|
|
1089
|
+
op_remove_instance: removeInstanceHandler,
|
|
1090
|
+
op_list_vaults: listVaultsHandler,
|
|
1091
|
+
op_list_items: listItemsHandler,
|
|
1092
|
+
op_search_items: searchItemsHandler,
|
|
1093
|
+
op_get_item: getItemHandler,
|
|
1094
|
+
op_get_secret: getSecretHandler,
|
|
1095
|
+
op_generate_password: generatePasswordHandler,
|
|
1096
|
+
op_create_item: createItemHandler,
|
|
1097
|
+
op_update_item: updateItemHandler,
|
|
1098
|
+
op_delete_item: deleteItemHandler,
|
|
1099
|
+
op_create_vault: createVaultHandler,
|
|
1100
|
+
op_update_vault: updateVaultHandler,
|
|
1101
|
+
op_delete_vault: deleteVaultHandler,
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// Drift guard: every declared tool must have a handler and vice versa. This is
|
|
1105
|
+
// evaluated at module load so a missing/extra handler fails fast.
|
|
1106
|
+
(function assertHandlerCompleteness() {
|
|
1107
|
+
const toolNames = new Set(TOOL_DEFINITIONS.map((t) => t.name));
|
|
1108
|
+
const handlerNames = new Set(Object.keys(HANDLERS));
|
|
1109
|
+
for (const name of toolNames) {
|
|
1110
|
+
if (!handlerNames.has(name)) {
|
|
1111
|
+
throw new Error(`Tool "${name}" is declared but has no handler.`);
|
|
867
1112
|
}
|
|
1113
|
+
}
|
|
1114
|
+
for (const name of handlerNames) {
|
|
1115
|
+
if (!toolNames.has(name)) {
|
|
1116
|
+
throw new Error(`Handler "${name}" exists but is not declared in TOOL_DEFINITIONS.`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
})();
|
|
1120
|
+
|
|
1121
|
+
// ============================================================================
|
|
1122
|
+
// MCP server wiring
|
|
1123
|
+
// ============================================================================
|
|
868
1124
|
|
|
869
|
-
|
|
870
|
-
|
|
1125
|
+
const server = new Server(
|
|
1126
|
+
{ name: INTEGRATION_NAME, version: PKG_VERSION },
|
|
1127
|
+
{ capabilities: { tools: {} } },
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1131
|
+
tools: TOOL_DEFINITIONS,
|
|
1132
|
+
}));
|
|
1133
|
+
|
|
1134
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1135
|
+
const { name, arguments: args } = request.params;
|
|
1136
|
+
const handler = HANDLERS[name];
|
|
1137
|
+
if (!handler) {
|
|
871
1138
|
return {
|
|
872
|
-
content: [{ type: "text", text: `
|
|
1139
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
1140
|
+
isError: true,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
const result = await handler(args || {});
|
|
1145
|
+
return { content: [{ type: "text", text: String(result) }] };
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
return {
|
|
1148
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
873
1149
|
isError: true,
|
|
874
1150
|
};
|
|
875
1151
|
}
|
|
@@ -887,13 +1163,38 @@ if (require.main === module) {
|
|
|
887
1163
|
});
|
|
888
1164
|
}
|
|
889
1165
|
|
|
890
|
-
//
|
|
1166
|
+
// ============================================================================
|
|
1167
|
+
// Test exports
|
|
1168
|
+
// ============================================================================
|
|
1169
|
+
|
|
891
1170
|
if (typeof module !== "undefined") {
|
|
892
1171
|
module.exports = {
|
|
1172
|
+
// config
|
|
1173
|
+
CONFIG_DIR,
|
|
1174
|
+
CONFIG_PATH,
|
|
893
1175
|
loadConfigFile,
|
|
894
1176
|
saveConfigFile,
|
|
1177
|
+
normalizeInstance,
|
|
1178
|
+
// instance resolution
|
|
895
1179
|
getInstanceByName,
|
|
1180
|
+
// helpers
|
|
1181
|
+
collect,
|
|
1182
|
+
resolveVault,
|
|
1183
|
+
resolveItem,
|
|
896
1184
|
redactField,
|
|
897
1185
|
formatItem,
|
|
1186
|
+
searchItems,
|
|
1187
|
+
// handler map + tool list (for drift guard tests)
|
|
1188
|
+
TOOL_DEFINITIONS,
|
|
1189
|
+
HANDLERS,
|
|
1190
|
+
// direct handler access for unit tests that don't need the MCP transport
|
|
1191
|
+
listInstancesHandler,
|
|
1192
|
+
listVaultsHandler,
|
|
1193
|
+
listItemsHandler,
|
|
1194
|
+
searchItemsHandler,
|
|
1195
|
+
getItemHandler,
|
|
1196
|
+
generatePasswordHandler,
|
|
1197
|
+
updateItemHandler,
|
|
1198
|
+
updateVaultHandler,
|
|
898
1199
|
};
|
|
899
1200
|
}
|