@rui.branco/1password-mcp 0.1.1 → 0.1.2
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 +83 -23
- package/package.json +2 -2
- package/setup.js +93 -8
package/README.md
CHANGED
|
@@ -16,33 +16,44 @@ Built to mirror the multi-instance pattern of [jira-mcp](https://github.com/rui-
|
|
|
16
16
|
### Prerequisites
|
|
17
17
|
|
|
18
18
|
- Node.js 18+
|
|
19
|
-
-
|
|
19
|
+
- Either:
|
|
20
|
+
- **The 1Password 8 desktop app** (recommended — no admin permissions needed, and you get access to all vaults you can already see), or
|
|
21
|
+
- A 1Password **service account** token (needs admin to provision, scoped to shared vaults only — good for CI/headless use)
|
|
20
22
|
|
|
21
|
-
### Step 1:
|
|
22
|
-
|
|
23
|
-
1. Go to <https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount>
|
|
24
|
-
2. Create a service account and grant it access to the vaults you want to use
|
|
25
|
-
3. Copy the token — it starts with `ops_`
|
|
26
|
-
|
|
27
|
-
### Step 2: Configure
|
|
23
|
+
### Step 1: Install and register the MCP
|
|
28
24
|
|
|
29
25
|
```bash
|
|
30
|
-
|
|
26
|
+
claude mcp add --scope user --transport stdio 1password -- npx -y @rui.branco/1password-mcp
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
### Step 2: Run setup
|
|
34
30
|
|
|
35
31
|
```bash
|
|
36
|
-
|
|
32
|
+
npx @rui.branco/1password-mcp setup
|
|
37
33
|
```
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
The setup wizard walks you through picking an auth mode and validates the connection before saving.
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
### Step 3 (desktop auth only): One-time 1Password 8 toggles
|
|
38
|
+
|
|
39
|
+
Desktop auth delegates unlock to the 1Password 8 desktop app, so you need to enable the SDK integration **once** per machine:
|
|
40
|
+
|
|
41
|
+
1. Open the **1Password 8 desktop app** (not `my.1password.com` in the browser).
|
|
42
|
+
2. Open **Settings** (cmd+, on Mac / ctrl+, on Windows).
|
|
43
|
+
3. Click the **Developer** tab.
|
|
44
|
+
4. Under *Command-Line Interface (CLI)*, tick **"Integrate with 1Password CLI"**.
|
|
45
|
+
5. Under *Integrate with the 1Password SDKs*, tick **"Integrate with other apps"**. ← **This is the critical one.** Without it, the SDK cannot unlock and every call will fail with `DesktopSessionExpiredError`.
|
|
46
|
+
6. Optional: **Settings → Security → Unlock using Touch ID** so the OS prompt can be cleared with your fingerprint.
|
|
47
|
+
|
|
48
|
+
The first time the MCP actually calls 1Password, your OS will pop an authorization prompt asking whether `1password-mcp-setup` (during the wizard) and `1password-mcp` (at runtime) can talk to 1Password. Click Approve.
|
|
49
|
+
|
|
50
|
+
### Finding your account name
|
|
51
|
+
|
|
52
|
+
When the wizard asks for "Account name", open the 1Password 8 desktop app and click the **account switcher** in the top-left. The signin address shown next to your name is what you want — it looks like `my.1password.com`, `my-team.1password.com`, or similar.
|
|
53
|
+
|
|
54
|
+
### Step 4: Reconnect
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
Run `/mcp` in Claude Code to reconnect the 1password server and pick up the new config.
|
|
46
57
|
|
|
47
58
|
## Tools
|
|
48
59
|
|
package/index.js
CHANGED
|
@@ -77,7 +77,10 @@ const rawConfig = fs.existsSync(configPath) ? loadConfigFile() : { instances: []
|
|
|
77
77
|
|
|
78
78
|
let instances = (rawConfig.instances || []).map((i) => ({
|
|
79
79
|
name: i.name,
|
|
80
|
+
// "serviceAccount" (default, needs ops_ token) or "desktop" (uses 1Password 8 app biometric unlock)
|
|
81
|
+
authType: i.authType || (i.token ? "serviceAccount" : "desktop"),
|
|
80
82
|
token: i.token,
|
|
83
|
+
accountName: i.accountName,
|
|
81
84
|
description: i.description || "",
|
|
82
85
|
}));
|
|
83
86
|
|
|
@@ -105,11 +108,24 @@ async function getClient(instance) {
|
|
|
105
108
|
);
|
|
106
109
|
}
|
|
107
110
|
if (clientCache.has(instance.name)) return clientCache.get(instance.name);
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
|
|
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;
|
|
110
125
|
}
|
|
126
|
+
|
|
111
127
|
const client = await sdk.createClient({
|
|
112
|
-
auth
|
|
128
|
+
auth,
|
|
113
129
|
integrationName: INTEGRATION_NAME,
|
|
114
130
|
integrationVersion: `v${PKG_VERSION}`,
|
|
115
131
|
});
|
|
@@ -203,14 +219,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
203
219
|
{
|
|
204
220
|
name: "op_add_instance",
|
|
205
221
|
description:
|
|
206
|
-
"Add or update a 1Password
|
|
222
|
+
"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.",
|
|
207
223
|
inputSchema: {
|
|
208
224
|
type: "object",
|
|
209
225
|
properties: {
|
|
210
226
|
name: { type: "string", description: "Unique name (e.g., 'work', 'personal')." },
|
|
227
|
+
authType: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: "'serviceAccount' (default) or 'desktop'.",
|
|
230
|
+
},
|
|
211
231
|
token: {
|
|
212
232
|
type: "string",
|
|
213
|
-
description: "
|
|
233
|
+
description: "Service account token (ops_...) — required for authType=serviceAccount.",
|
|
234
|
+
},
|
|
235
|
+
accountName: {
|
|
236
|
+
type: "string",
|
|
237
|
+
description: "1Password account (e.g. 'my.1password.com' or shorthand) — required for authType=desktop. Uses the logged-in 1Password 8 app.",
|
|
214
238
|
},
|
|
215
239
|
description: { type: "string", description: "Optional human-readable description." },
|
|
216
240
|
setDefault: {
|
|
@@ -218,7 +242,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
218
242
|
description: "Set this instance as the default (default: false).",
|
|
219
243
|
},
|
|
220
244
|
},
|
|
221
|
-
required: ["name"
|
|
245
|
+
required: ["name"],
|
|
222
246
|
},
|
|
223
247
|
},
|
|
224
248
|
{
|
|
@@ -433,33 +457,67 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
433
457
|
for (const inst of instances) {
|
|
434
458
|
const isDefault = inst.name === def ? " **(default)**" : "";
|
|
435
459
|
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
436
|
-
|
|
460
|
+
const auth =
|
|
461
|
+
inst.authType === "desktop"
|
|
462
|
+
? ` (desktop: ${inst.accountName})`
|
|
463
|
+
: " (serviceAccount)";
|
|
464
|
+
text += `- **${inst.name}**${isDefault}${auth}${desc}\n`;
|
|
437
465
|
}
|
|
438
466
|
return { content: [{ type: "text", text }] };
|
|
439
467
|
}
|
|
440
468
|
|
|
441
469
|
if (name === "op_add_instance") {
|
|
442
470
|
const instName = args.name.trim();
|
|
443
|
-
|
|
471
|
+
const authType = args.authType || (args.token ? "serviceAccount" : "desktop");
|
|
472
|
+
|
|
473
|
+
if (authType === "serviceAccount") {
|
|
474
|
+
if (!args.token) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: "serviceAccount authType requires a token." }],
|
|
477
|
+
isError: true,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (!/^ops_[A-Za-z0-9_\-]+/.test(args.token)) {
|
|
481
|
+
return {
|
|
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 {
|
|
444
504
|
return {
|
|
445
|
-
content: [
|
|
446
|
-
{
|
|
447
|
-
type: "text",
|
|
448
|
-
text: "Token does not look like a 1Password service account token (expected prefix 'ops_').",
|
|
449
|
-
},
|
|
450
|
-
],
|
|
505
|
+
content: [{ type: "text", text: `Unknown authType "${authType}".` }],
|
|
451
506
|
isError: true,
|
|
452
507
|
};
|
|
453
508
|
}
|
|
454
509
|
|
|
455
|
-
// Validate by creating a client and listing
|
|
510
|
+
// Validate by creating a client and listing one vault.
|
|
456
511
|
try {
|
|
512
|
+
const probeAuth =
|
|
513
|
+
authType === "desktop"
|
|
514
|
+
? new sdk.DesktopAuth(args.accountName)
|
|
515
|
+
: args.token;
|
|
457
516
|
const tempClient = await sdk.createClient({
|
|
458
|
-
auth:
|
|
517
|
+
auth: probeAuth,
|
|
459
518
|
integrationName: INTEGRATION_NAME,
|
|
460
519
|
integrationVersion: `v${PKG_VERSION}`,
|
|
461
520
|
});
|
|
462
|
-
// consume iterator to force a real API call
|
|
463
521
|
const probe = [];
|
|
464
522
|
for await (const v of await tempClient.vaults.list()) {
|
|
465
523
|
probe.push(v);
|
|
@@ -467,16 +525,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
467
525
|
}
|
|
468
526
|
} catch (e) {
|
|
469
527
|
return {
|
|
470
|
-
content: [
|
|
471
|
-
{ type: "text", text: `Token validation failed: ${e.message}` },
|
|
472
|
-
],
|
|
528
|
+
content: [{ type: "text", text: `Auth validation failed: ${e.message}` }],
|
|
473
529
|
isError: true,
|
|
474
530
|
};
|
|
475
531
|
}
|
|
476
532
|
|
|
477
533
|
const newInst = {
|
|
478
534
|
name: instName,
|
|
479
|
-
|
|
535
|
+
authType,
|
|
536
|
+
token: authType === "serviceAccount" ? args.token : undefined,
|
|
537
|
+
accountName: authType === "desktop" ? args.accountName : undefined,
|
|
480
538
|
description: args.description || "",
|
|
481
539
|
};
|
|
482
540
|
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
@@ -487,7 +545,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
487
545
|
const savedConfig = loadConfigFile();
|
|
488
546
|
if (!savedConfig.instances) savedConfig.instances = [];
|
|
489
547
|
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
490
|
-
const toSave = { name: instName,
|
|
548
|
+
const toSave = { name: instName, authType };
|
|
549
|
+
if (authType === "serviceAccount") toSave.token = args.token;
|
|
550
|
+
if (authType === "desktop") toSave.accountName = args.accountName;
|
|
491
551
|
if (args.description) toSave.description = args.description;
|
|
492
552
|
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
493
553
|
else savedConfig.instances.push(toSave);
|
|
@@ -500,7 +560,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
500
560
|
content: [
|
|
501
561
|
{
|
|
502
562
|
type: "text",
|
|
503
|
-
text: `${existingIdx >= 0 ? "Updated" : "Added"} instance "${instName}".${args.setDefault ? " Set as default." : ""}`,
|
|
563
|
+
text: `${existingIdx >= 0 ? "Updated" : "Added"} instance "${instName}" (${authType}).${args.setDefault ? " Set as default." : ""}`,
|
|
504
564
|
},
|
|
505
565
|
],
|
|
506
566
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rui.branco/1password-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "1Password MCP server for Claude Code - list vaults and items, get secrets, generate passwords, manage items via service accounts. Multi-instance like jira-mcp.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"author": "Rui Branco",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@1password/sdk": "^0.
|
|
27
|
+
"@1password/sdk": "^0.4.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/setup.js
CHANGED
|
@@ -28,19 +28,37 @@ function saveConfig(cfg) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Non-interactive: setup add <name> <token> [description]
|
|
31
|
+
// setup add-desktop <name> <accountName> [description]
|
|
31
32
|
if (args[0] === "add" && args.length >= 3) {
|
|
32
33
|
const [, name, token, ...descArgs] = args;
|
|
33
34
|
const description = descArgs.join(" ").trim();
|
|
34
35
|
const config = loadConfig() || {};
|
|
35
36
|
if (!config.instances) config.instances = [];
|
|
36
37
|
const existing = config.instances.findIndex((i) => i.name === name);
|
|
37
|
-
const instance = { name, token };
|
|
38
|
+
const instance = { name, authType: "serviceAccount", token };
|
|
38
39
|
if (description) instance.description = description;
|
|
39
40
|
if (existing >= 0) config.instances[existing] = instance;
|
|
40
41
|
else config.instances.push(instance);
|
|
41
42
|
if (!config.defaultInstance) config.defaultInstance = name;
|
|
42
43
|
saveConfig(config);
|
|
43
|
-
console.log(`Instance "${name}" saved to ${configPath}`);
|
|
44
|
+
console.log(`Instance "${name}" saved to ${configPath} (serviceAccount)`);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (args[0] === "add-desktop" && args.length >= 3) {
|
|
49
|
+
const [, name, accountName, ...descArgs] = args;
|
|
50
|
+
const description = descArgs.join(" ").trim();
|
|
51
|
+
const config = loadConfig() || {};
|
|
52
|
+
if (!config.instances) config.instances = [];
|
|
53
|
+
const existing = config.instances.findIndex((i) => i.name === name);
|
|
54
|
+
const instance = { name, authType: "desktop", accountName };
|
|
55
|
+
if (description) instance.description = description;
|
|
56
|
+
if (existing >= 0) config.instances[existing] = instance;
|
|
57
|
+
else config.instances.push(instance);
|
|
58
|
+
if (!config.defaultInstance) config.defaultInstance = name;
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
console.log(`Instance "${name}" saved to ${configPath} (desktop: ${accountName})`);
|
|
61
|
+
console.log("Make sure the 1Password 8 desktop app is installed and the CLI integration is enabled (Settings → Developer → Connect with 1Password CLI).");
|
|
44
62
|
process.exit(0);
|
|
45
63
|
}
|
|
46
64
|
|
|
@@ -88,19 +106,86 @@ async function setup() {
|
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
console.log("
|
|
92
|
-
console.log("1.
|
|
93
|
-
console.log("2.
|
|
94
|
-
|
|
109
|
+
console.log("Pick auth mode:");
|
|
110
|
+
console.log(" 1. Desktop app (recommended for personal use — uses 1Password 8 biometric unlock, full vault access)");
|
|
111
|
+
console.log(" 2. Service account token (recommended for CI/shared — needs admin to provision, scoped vaults only)\n");
|
|
112
|
+
const mode = (await ask("Auth mode (1/2, default 1): ")).trim() || "1";
|
|
95
113
|
|
|
96
114
|
const name = (await ask("Instance name (e.g., work): ")).trim();
|
|
97
|
-
const token = (await ask("Service account token: ")).trim();
|
|
98
115
|
const description = (await ask("Description (optional): ")).trim();
|
|
99
116
|
|
|
100
117
|
const config = existing || { instances: [] };
|
|
101
118
|
if (!config.instances) config.instances = [];
|
|
102
119
|
const idx = config.instances.findIndex((i) => i.name === name);
|
|
103
|
-
|
|
120
|
+
let instance;
|
|
121
|
+
|
|
122
|
+
if (mode === "2") {
|
|
123
|
+
console.log("\nCreate a service account token at:");
|
|
124
|
+
console.log(" https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount");
|
|
125
|
+
const token = (await ask("Service account token (ops_...): ")).trim();
|
|
126
|
+
instance = { name, authType: "serviceAccount", token };
|
|
127
|
+
} else {
|
|
128
|
+
console.log("\n=== One-time 1Password 8 setup (required for desktop auth) ===");
|
|
129
|
+
console.log("");
|
|
130
|
+
console.log("Open the 1Password 8 desktop app (not the browser — the Mac/Windows app).");
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(" 1. Open Settings (cmd+, on Mac / ctrl+, on Windows)");
|
|
133
|
+
console.log(" 2. Click the 'Developer' tab on the left");
|
|
134
|
+
console.log(" 3. Tick 'Integrate with 1Password CLI'");
|
|
135
|
+
console.log(" (under 'Command-Line Interface (CLI)')");
|
|
136
|
+
console.log(" 4. Tick 'Integrate with other apps'");
|
|
137
|
+
console.log(" (under 'Integrate with the 1Password SDKs')");
|
|
138
|
+
console.log(" ** THIS IS THE KEY ONE — without it the SDK can't unlock. **");
|
|
139
|
+
console.log(" 5. (Optional) Settings → Security → 'Unlock using Touch ID'");
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log("The first time the MCP calls 1Password you'll get a system prompt");
|
|
142
|
+
console.log("asking to authorize '1password-mcp' — click Approve.");
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log("=== Finding your account name ===");
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log("Open 1Password 8 → click the account switcher in the top-left.");
|
|
147
|
+
console.log("The signin address shown next to your name is what you want here,");
|
|
148
|
+
console.log("e.g. 'my.1password.com' or 'my-team.1password.com'.");
|
|
149
|
+
console.log("");
|
|
150
|
+
const accountName = (await ask("Account name (e.g., my.1password.com): ")).trim();
|
|
151
|
+
if (!accountName) {
|
|
152
|
+
console.error("Account name is required. Aborting.");
|
|
153
|
+
rl.close();
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
instance = { name, authType: "desktop", accountName };
|
|
157
|
+
|
|
158
|
+
// Validate by actually trying to talk to the desktop app. This will pop the
|
|
159
|
+
// OS-level authorization prompt on first use.
|
|
160
|
+
console.log("\nValidating with 1Password desktop app (you may get an auth prompt)…");
|
|
161
|
+
try {
|
|
162
|
+
const sdk = require("@1password/sdk");
|
|
163
|
+
const client = await sdk.createClient({
|
|
164
|
+
auth: new sdk.DesktopAuth(accountName),
|
|
165
|
+
integrationName: "1password-mcp-setup",
|
|
166
|
+
integrationVersion: "v1",
|
|
167
|
+
});
|
|
168
|
+
const probe = [];
|
|
169
|
+
for await (const v of await client.vaults.list()) {
|
|
170
|
+
probe.push(v);
|
|
171
|
+
if (probe.length >= 1) break;
|
|
172
|
+
}
|
|
173
|
+
console.log(`Validated. Saw vault: ${probe[0]?.title || "(none visible)"}.`);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error("");
|
|
176
|
+
console.error("Validation failed: " + e.message);
|
|
177
|
+
console.error("");
|
|
178
|
+
console.error("Common causes:");
|
|
179
|
+
console.error(" - 'Integrate with other apps' is NOT ticked in 1Password Settings → Developer");
|
|
180
|
+
console.error(" - 1Password 8 desktop app is not running or locked");
|
|
181
|
+
console.error(" - Account name is wrong (check the account switcher in 1Password 8)");
|
|
182
|
+
console.error("");
|
|
183
|
+
console.error("Fix the issue and re-run this setup.");
|
|
184
|
+
rl.close();
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
104
189
|
if (description) instance.description = description;
|
|
105
190
|
if (idx >= 0) config.instances[idx] = instance;
|
|
106
191
|
else config.instances.push(instance);
|