@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.
Files changed (4) hide show
  1. package/README.md +27 -16
  2. package/index.js +83 -23
  3. package/package.json +2 -2
  4. 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
- - A 1Password account with permission to create [service accounts](https://developer.1password.com/docs/service-accounts/)
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: Create a service account token
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
- node $HOME/WebstormProjects/1password-mcp/setup.js
26
+ claude mcp add --scope user --transport stdio 1password -- npx -y @rui.branco/1password-mcp
31
27
  ```
32
28
 
33
- Or non-interactively:
29
+ ### Step 2: Run setup
34
30
 
35
31
  ```bash
36
- node $HOME/WebstormProjects/1password-mcp/setup.js add work ops_ABC... "ITO service account"
32
+ npx @rui.branco/1password-mcp setup
37
33
  ```
38
34
 
39
- ### Step 3: Add to Claude Code
35
+ The setup wizard walks you through picking an auth mode and validates the connection before saving.
40
36
 
41
- ```bash
42
- claude mcp add --transport stdio 1password -- node $HOME/WebstormProjects/1password-mcp/index.js
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
- Restart Claude Code and run `/mcp` to verify.
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
- if (!instance.token) {
109
- throw new Error(`Instance "${instance.name}" has no service account token.`);
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: instance.token,
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 service-account instance. Provide name + token (and optional description). Token is stored in ~/.config/1password-mcp/config.json with 0600 permissions.",
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: "1Password service account token (ops_... prefix).",
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", "token"],
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
- text += `- **${inst.name}**${isDefault}${desc}\n`;
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
- if (!/^ops_[A-Za-z0-9_\-]+/.test(args.token)) {
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 vaults.
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: args.token,
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
- token: args.token,
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, token: args.token };
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.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.3.1",
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("To create a 1Password service account token:");
92
- console.log("1. Go to https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount");
93
- console.log("2. Create a service account and grant it access to the vaults you need");
94
- console.log("3. Copy the token (starts with 'ops_')\n");
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
- const instance = { name, token };
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);