@rui.branco/1password-mcp 0.1.1
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 +86 -0
- package/index.js +839 -0
- package/package.json +30 -0
- package/setup.js +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 1Password MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for [1Password](https://1password.com/) that runs **entirely locally** — no external services, no telemetry, no leaks. Uses the official [`@1password/sdk`](https://www.npmjs.com/package/@1password/sdk) with a 1Password Service Account token.
|
|
4
|
+
|
|
5
|
+
Built to mirror the multi-instance pattern of [jira-mcp](https://github.com/rui-branco/jira-mcp) so you can point the same Claude Code session at multiple 1Password accounts (e.g. `work` and `personal`).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-instance** — add several service accounts, switch per tool call via `instance` parameter
|
|
10
|
+
- **Local only** — all calls go directly from your machine to 1Password over TLS; the config file is stored `0600` in `~/.config/1password-mcp/config.json`
|
|
11
|
+
- **Redaction by default** — concealed fields (passwords, TOTP) are redacted unless you explicitly pass `includeConcealed: true`
|
|
12
|
+
- **Read + write** — list vaults/items, fetch full items, resolve `op://` references, generate passwords, create/delete items, create/delete vaults
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 18+
|
|
19
|
+
- A 1Password account with permission to create [service accounts](https://developer.1password.com/docs/service-accounts/)
|
|
20
|
+
|
|
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
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node $HOME/WebstormProjects/1password-mcp/setup.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or non-interactively:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node $HOME/WebstormProjects/1password-mcp/setup.js add work ops_ABC... "ITO service account"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Step 3: Add to Claude Code
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
claude mcp add --transport stdio 1password -- node $HOME/WebstormProjects/1password-mcp/index.js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Restart Claude Code and run `/mcp` to verify.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
| Tool | Description |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `op_list_instances` | List configured service accounts (tokens never returned) |
|
|
52
|
+
| `op_add_instance` | Add/update an instance. Validates the token against the API. |
|
|
53
|
+
| `op_remove_instance` | Remove an instance by name |
|
|
54
|
+
| `op_list_vaults` | List vaults visible to the service account |
|
|
55
|
+
| `op_list_items` | List items in a vault (by vault id or title) |
|
|
56
|
+
| `op_search_items` | Title-substring search across one or all vaults |
|
|
57
|
+
| `op_get_item` | Fetch a full item. Concealed fields redacted unless `includeConcealed=true`. |
|
|
58
|
+
| `op_get_secret` | Resolve a `op://Vault/Item/field` secret reference |
|
|
59
|
+
| `op_generate_password` | Generate Random / Memorable / Pin passwords via the 1Password generator |
|
|
60
|
+
| `op_create_item` | Create a new item with fields, tags, and websites |
|
|
61
|
+
| `op_delete_item` | Delete an item |
|
|
62
|
+
| `op_create_vault` | Create a new vault |
|
|
63
|
+
| `op_delete_vault` | Delete a vault |
|
|
64
|
+
|
|
65
|
+
All tools that talk to 1Password accept an optional `instance` parameter. Omit it to use the default instance.
|
|
66
|
+
|
|
67
|
+
## Security notes
|
|
68
|
+
|
|
69
|
+
- Tokens live only in `~/.config/1password-mcp/config.json` (chmod 0600) and in the SDK client process. They are never logged or transmitted anywhere except to `*.1password.com`.
|
|
70
|
+
- Service accounts only see vaults that have been explicitly shared with them — personal vaults are inaccessible by design.
|
|
71
|
+
- Concealed fields are redacted by default in tool output so the model cannot accidentally quote a password back. Pass `includeConcealed: true` only when you actually need the cleartext.
|
|
72
|
+
|
|
73
|
+
## Config shape
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"instances": [
|
|
78
|
+
{ "name": "work", "token": "ops_...", "description": "ITO service account" }
|
|
79
|
+
],
|
|
80
|
+
"defaultInstance": "work"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Handle setup subcommand
|
|
4
|
+
if (process.argv[2] === "setup") {
|
|
5
|
+
require("./setup.js");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
10
|
+
const {
|
|
11
|
+
StdioServerTransport,
|
|
12
|
+
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
+
const {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const sdk = require("@1password/sdk");
|
|
20
|
+
const { spawn, execSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
const PKG_VERSION = require("./package.json").version;
|
|
23
|
+
const INTEGRATION_NAME = "1password-mcp";
|
|
24
|
+
|
|
25
|
+
// Auto-update: check GitHub for new commits, install in background
|
|
26
|
+
const GITHUB_REPO = "rui-branco/1password-mcp";
|
|
27
|
+
const INSTALLED_SHA_FILE = path.join(__dirname, ".installed-sha");
|
|
28
|
+
try {
|
|
29
|
+
const localSha = fs.existsSync(INSTALLED_SHA_FILE)
|
|
30
|
+
? fs.readFileSync(INSTALLED_SHA_FILE, "utf-8").trim()
|
|
31
|
+
: "";
|
|
32
|
+
const remoteSha = execSync(
|
|
33
|
+
`git ls-remote https://github.com/${GITHUB_REPO}.git HEAD`,
|
|
34
|
+
{ stdio: "pipe", timeout: 5000 },
|
|
35
|
+
)
|
|
36
|
+
.toString()
|
|
37
|
+
.split("\t")[0]
|
|
38
|
+
.trim();
|
|
39
|
+
if (remoteSha && remoteSha !== localSha) {
|
|
40
|
+
const child = spawn(
|
|
41
|
+
"sh",
|
|
42
|
+
[
|
|
43
|
+
"-c",
|
|
44
|
+
`npm install -g git+ssh://git@github.com/${GITHUB_REPO}.git && echo "${remoteSha}" > "${INSTALLED_SHA_FILE}"`,
|
|
45
|
+
],
|
|
46
|
+
{ stdio: "ignore", detached: true },
|
|
47
|
+
);
|
|
48
|
+
child.unref();
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
|
|
52
|
+
// ============ CONFIG ============
|
|
53
|
+
|
|
54
|
+
const configDir = path.join(process.env.HOME, ".config/1password-mcp");
|
|
55
|
+
const configPath = path.join(configDir, "config.json");
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(configDir)) {
|
|
58
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadConfigFile() {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveConfigFile(cfg) {
|
|
70
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
71
|
+
try {
|
|
72
|
+
fs.chmodSync(configPath, 0o600);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rawConfig = fs.existsSync(configPath) ? loadConfigFile() : { instances: [] };
|
|
77
|
+
|
|
78
|
+
let instances = (rawConfig.instances || []).map((i) => ({
|
|
79
|
+
name: i.name,
|
|
80
|
+
token: i.token,
|
|
81
|
+
description: i.description || "",
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const defaultInstance =
|
|
85
|
+
(rawConfig.defaultInstance && instances.find((i) => i.name === rawConfig.defaultInstance)) ||
|
|
86
|
+
instances[0] ||
|
|
87
|
+
null;
|
|
88
|
+
|
|
89
|
+
function getInstanceByName(name) {
|
|
90
|
+
if (!name) return defaultInstance;
|
|
91
|
+
return instances.find((i) => i.name === name) || defaultInstance;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============ 1PASSWORD CLIENT ============
|
|
95
|
+
|
|
96
|
+
// Cache SDK clients per instance so we don't re-auth on every call. The SDK
|
|
97
|
+
// client holds a WebAssembly runtime internally, so recreating it every call
|
|
98
|
+
// would be wasteful.
|
|
99
|
+
const clientCache = new Map();
|
|
100
|
+
|
|
101
|
+
async function getClient(instance) {
|
|
102
|
+
if (!instance) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"No 1Password instance configured. Run `1password-mcp setup` or call op_add_instance.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
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.`);
|
|
110
|
+
}
|
|
111
|
+
const client = await sdk.createClient({
|
|
112
|
+
auth: instance.token,
|
|
113
|
+
integrationName: INTEGRATION_NAME,
|
|
114
|
+
integrationVersion: `v${PKG_VERSION}`,
|
|
115
|
+
});
|
|
116
|
+
clientCache.set(instance.name, client);
|
|
117
|
+
return client;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function invalidateClient(name) {
|
|
121
|
+
clientCache.delete(name);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============ HELPERS ============
|
|
125
|
+
|
|
126
|
+
// Pick a vault by id OR by title (case-insensitive exact match, then prefix).
|
|
127
|
+
async function resolveVault(client, vaultRef) {
|
|
128
|
+
if (!vaultRef) return null;
|
|
129
|
+
const vaults = [];
|
|
130
|
+
for await (const v of await client.vaults.list()) vaults.push(v);
|
|
131
|
+
const byId = vaults.find((v) => v.id === vaultRef);
|
|
132
|
+
if (byId) return byId;
|
|
133
|
+
const lower = String(vaultRef).toLowerCase();
|
|
134
|
+
const exact = vaults.find((v) => (v.title || "").toLowerCase() === lower);
|
|
135
|
+
if (exact) return exact;
|
|
136
|
+
const prefix = vaults.find((v) => (v.title || "").toLowerCase().startsWith(lower));
|
|
137
|
+
if (prefix) return prefix;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function resolveItem(client, vault, itemRef) {
|
|
142
|
+
if (!itemRef) return null;
|
|
143
|
+
const items = [];
|
|
144
|
+
for await (const it of await client.items.list(vault.id)) items.push(it);
|
|
145
|
+
const byId = items.find((i) => i.id === itemRef);
|
|
146
|
+
if (byId) return byId;
|
|
147
|
+
const lower = String(itemRef).toLowerCase();
|
|
148
|
+
const exact = items.find((i) => (i.title || "").toLowerCase() === lower);
|
|
149
|
+
if (exact) return exact;
|
|
150
|
+
const prefix = items.find((i) => (i.title || "").toLowerCase().startsWith(lower));
|
|
151
|
+
if (prefix) return prefix;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function redactField(field, includeConcealed) {
|
|
156
|
+
const out = {
|
|
157
|
+
id: field.id,
|
|
158
|
+
title: field.title,
|
|
159
|
+
type: field.fieldType,
|
|
160
|
+
sectionId: field.sectionId || undefined,
|
|
161
|
+
};
|
|
162
|
+
const isSecret = field.fieldType === "Concealed" || field.fieldType === "Totp";
|
|
163
|
+
if (!isSecret || includeConcealed) {
|
|
164
|
+
out.value = field.value;
|
|
165
|
+
if (field.details && field.details.type === "Otp" && field.details.content) {
|
|
166
|
+
out.otp = field.details.content.code || undefined;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
out.value = "[REDACTED]";
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatItem(item, includeConcealed) {
|
|
175
|
+
return {
|
|
176
|
+
id: item.id,
|
|
177
|
+
title: item.title,
|
|
178
|
+
category: item.category,
|
|
179
|
+
vaultId: item.vaultId,
|
|
180
|
+
tags: item.tags || [],
|
|
181
|
+
websites: (item.websites || []).map((w) => ({ url: w.url, label: w.label })),
|
|
182
|
+
fields: (item.fields || []).map((f) => redactField(f, includeConcealed)),
|
|
183
|
+
sections: (item.sections || []).map((s) => ({ id: s.id, title: s.title })),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============ MCP SERVER ============
|
|
188
|
+
|
|
189
|
+
const server = new Server(
|
|
190
|
+
{ name: INTEGRATION_NAME, version: PKG_VERSION },
|
|
191
|
+
{ capabilities: { tools: {} } },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
195
|
+
return {
|
|
196
|
+
tools: [
|
|
197
|
+
{
|
|
198
|
+
name: "op_list_instances",
|
|
199
|
+
description:
|
|
200
|
+
"List configured 1Password instances (service accounts). Tokens are never returned.",
|
|
201
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "op_add_instance",
|
|
205
|
+
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.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
name: { type: "string", description: "Unique name (e.g., 'work', 'personal')." },
|
|
211
|
+
token: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "1Password service account token (ops_... prefix).",
|
|
214
|
+
},
|
|
215
|
+
description: { type: "string", description: "Optional human-readable description." },
|
|
216
|
+
setDefault: {
|
|
217
|
+
type: "boolean",
|
|
218
|
+
description: "Set this instance as the default (default: false).",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ["name", "token"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "op_remove_instance",
|
|
226
|
+
description: "Remove a 1Password instance by name. Cannot remove the last one.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
name: { type: "string", description: "Instance name to remove." },
|
|
231
|
+
},
|
|
232
|
+
required: ["name"],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "op_list_vaults",
|
|
237
|
+
description: "List all vaults visible to the given instance's service account.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
242
|
+
},
|
|
243
|
+
required: [],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "op_list_items",
|
|
248
|
+
description:
|
|
249
|
+
"List items in a vault. `vault` accepts a vault id or title (case-insensitive exact, then prefix).",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
254
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
255
|
+
},
|
|
256
|
+
required: ["vault"],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: "op_search_items",
|
|
261
|
+
description:
|
|
262
|
+
"Search items by title across vaults. If `vault` is omitted, searches every vault the service account can see.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
query: { type: "string", description: "Case-insensitive substring of the item title." },
|
|
267
|
+
vault: { type: "string", description: "Optional vault id or title to scope the search." },
|
|
268
|
+
limit: { type: "number", description: "Max results (default 25)." },
|
|
269
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
270
|
+
},
|
|
271
|
+
required: ["query"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "op_get_item",
|
|
276
|
+
description:
|
|
277
|
+
"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.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
282
|
+
item: { type: "string", description: "Item id or title." },
|
|
283
|
+
includeConcealed: {
|
|
284
|
+
type: "boolean",
|
|
285
|
+
description: "Return concealed values in cleartext (default: false).",
|
|
286
|
+
},
|
|
287
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
288
|
+
},
|
|
289
|
+
required: ["vault", "item"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "op_get_secret",
|
|
294
|
+
description:
|
|
295
|
+
"Resolve a 1Password secret reference (op://vault/item/field) and return the value.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
reference: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "Secret reference like 'op://Private/GitHub/password'.",
|
|
302
|
+
},
|
|
303
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
304
|
+
},
|
|
305
|
+
required: ["reference"],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "op_generate_password",
|
|
310
|
+
description:
|
|
311
|
+
"Generate a password using 1Password's generator (Random / Memorable / Pin). Does not talk to the account.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
type: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "'Random' (default), 'Memorable', or 'Pin'.",
|
|
318
|
+
},
|
|
319
|
+
length: { type: "number", description: "Length for Random/Pin (default 20/8)." },
|
|
320
|
+
includeDigits: { type: "boolean", description: "Random only (default true)." },
|
|
321
|
+
includeSymbols: { type: "boolean", description: "Random only (default true)." },
|
|
322
|
+
wordCount: { type: "number", description: "Memorable only (default 5)." },
|
|
323
|
+
capitalize: { type: "boolean", description: "Memorable only (default true)." },
|
|
324
|
+
},
|
|
325
|
+
required: [],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "op_create_item",
|
|
330
|
+
description:
|
|
331
|
+
"Create a new item in a vault. Fields is an array of { title, type, value, sectionId? }. Type is Text | Concealed | Totp | Email | Url | Phone.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
336
|
+
title: { type: "string", description: "Item title." },
|
|
337
|
+
category: {
|
|
338
|
+
type: "string",
|
|
339
|
+
description:
|
|
340
|
+
"Item category (default: Login). One of: Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
|
|
341
|
+
},
|
|
342
|
+
fields: {
|
|
343
|
+
type: "array",
|
|
344
|
+
description: "Field definitions: [{ title, type, value, sectionId? }].",
|
|
345
|
+
items: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
title: { type: "string" },
|
|
349
|
+
type: { type: "string" },
|
|
350
|
+
value: { type: "string" },
|
|
351
|
+
sectionId: { type: "string" },
|
|
352
|
+
},
|
|
353
|
+
required: ["title", "type", "value"],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
tags: { type: "array", items: { type: "string" } },
|
|
357
|
+
websites: {
|
|
358
|
+
type: "array",
|
|
359
|
+
items: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
url: { type: "string" },
|
|
363
|
+
label: { type: "string" },
|
|
364
|
+
},
|
|
365
|
+
required: ["url"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
notes: { type: "string", description: "Optional notes (stored as a Text field called 'notesPlain')." },
|
|
369
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
370
|
+
},
|
|
371
|
+
required: ["vault", "title"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "op_delete_item",
|
|
376
|
+
description: "Delete an item by vault + item reference.",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: {
|
|
380
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
381
|
+
item: { type: "string", description: "Item id or title." },
|
|
382
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
383
|
+
},
|
|
384
|
+
required: ["vault", "item"],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "op_create_vault",
|
|
389
|
+
description: "Create a new vault.",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
title: { type: "string", description: "Vault title." },
|
|
394
|
+
description: { type: "string", description: "Optional description." },
|
|
395
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
396
|
+
},
|
|
397
|
+
required: ["title"],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "op_delete_vault",
|
|
402
|
+
description: "Delete a vault by id or title.",
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {
|
|
406
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
407
|
+
instance: { type: "string", description: "Instance name. Uses default if omitted." },
|
|
408
|
+
},
|
|
409
|
+
required: ["vault"],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
417
|
+
const { name, arguments: args } = request.params;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
if (name === "op_list_instances") {
|
|
421
|
+
if (instances.length === 0) {
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: "text",
|
|
426
|
+
text: "No instances configured. Run `1password-mcp setup` or call op_add_instance.",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const def = defaultInstance?.name;
|
|
432
|
+
let text = `# 1Password Instances (${instances.length})\n\n`;
|
|
433
|
+
for (const inst of instances) {
|
|
434
|
+
const isDefault = inst.name === def ? " **(default)**" : "";
|
|
435
|
+
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
436
|
+
text += `- **${inst.name}**${isDefault}${desc}\n`;
|
|
437
|
+
}
|
|
438
|
+
return { content: [{ type: "text", text }] };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (name === "op_add_instance") {
|
|
442
|
+
const instName = args.name.trim();
|
|
443
|
+
if (!/^ops_[A-Za-z0-9_\-]+/.test(args.token)) {
|
|
444
|
+
return {
|
|
445
|
+
content: [
|
|
446
|
+
{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: "Token does not look like a 1Password service account token (expected prefix 'ops_').",
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
isError: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Validate by creating a client and listing vaults.
|
|
456
|
+
try {
|
|
457
|
+
const tempClient = await sdk.createClient({
|
|
458
|
+
auth: args.token,
|
|
459
|
+
integrationName: INTEGRATION_NAME,
|
|
460
|
+
integrationVersion: `v${PKG_VERSION}`,
|
|
461
|
+
});
|
|
462
|
+
// consume iterator to force a real API call
|
|
463
|
+
const probe = [];
|
|
464
|
+
for await (const v of await tempClient.vaults.list()) {
|
|
465
|
+
probe.push(v);
|
|
466
|
+
if (probe.length >= 1) break;
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{ type: "text", text: `Token validation failed: ${e.message}` },
|
|
472
|
+
],
|
|
473
|
+
isError: true,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const newInst = {
|
|
478
|
+
name: instName,
|
|
479
|
+
token: args.token,
|
|
480
|
+
description: args.description || "",
|
|
481
|
+
};
|
|
482
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
483
|
+
if (existingIdx >= 0) instances[existingIdx] = newInst;
|
|
484
|
+
else instances.push(newInst);
|
|
485
|
+
invalidateClient(instName);
|
|
486
|
+
|
|
487
|
+
const savedConfig = loadConfigFile();
|
|
488
|
+
if (!savedConfig.instances) savedConfig.instances = [];
|
|
489
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
490
|
+
const toSave = { name: instName, token: args.token };
|
|
491
|
+
if (args.description) toSave.description = args.description;
|
|
492
|
+
if (savedIdx >= 0) savedConfig.instances[savedIdx] = toSave;
|
|
493
|
+
else savedConfig.instances.push(toSave);
|
|
494
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
495
|
+
savedConfig.defaultInstance = instName;
|
|
496
|
+
}
|
|
497
|
+
saveConfigFile(savedConfig);
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
content: [
|
|
501
|
+
{
|
|
502
|
+
type: "text",
|
|
503
|
+
text: `${existingIdx >= 0 ? "Updated" : "Added"} instance "${instName}".${args.setDefault ? " Set as default." : ""}`,
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (name === "op_remove_instance") {
|
|
510
|
+
if (instances.length <= 1) {
|
|
511
|
+
return {
|
|
512
|
+
content: [{ type: "text", text: "Cannot remove the last remaining instance." }],
|
|
513
|
+
isError: true,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const idx = instances.findIndex((i) => i.name === args.name);
|
|
517
|
+
if (idx < 0) {
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: `Instance "${args.name}" not found.` }],
|
|
520
|
+
isError: true,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
instances.splice(idx, 1);
|
|
524
|
+
invalidateClient(args.name);
|
|
525
|
+
const savedConfig = loadConfigFile();
|
|
526
|
+
if (savedConfig.instances) {
|
|
527
|
+
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== args.name);
|
|
528
|
+
if (savedConfig.defaultInstance === args.name) {
|
|
529
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
530
|
+
}
|
|
531
|
+
saveConfigFile(savedConfig);
|
|
532
|
+
}
|
|
533
|
+
return { content: [{ type: "text", text: `Removed instance "${args.name}".` }] };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (name === "op_list_vaults") {
|
|
537
|
+
const inst = getInstanceByName(args.instance);
|
|
538
|
+
const client = await getClient(inst);
|
|
539
|
+
const vaults = [];
|
|
540
|
+
for await (const v of await client.vaults.list()) vaults.push(v);
|
|
541
|
+
if (vaults.length === 0) {
|
|
542
|
+
return { content: [{ type: "text", text: "No vaults visible to this service account." }] };
|
|
543
|
+
}
|
|
544
|
+
let text = `# Vaults (${vaults.length})\n\n`;
|
|
545
|
+
for (const v of vaults) text += `- **${v.title}** (id: ${v.id})\n`;
|
|
546
|
+
return { content: [{ type: "text", text }] };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (name === "op_list_items") {
|
|
550
|
+
const inst = getInstanceByName(args.instance);
|
|
551
|
+
const client = await getClient(inst);
|
|
552
|
+
const vault = await resolveVault(client, args.vault);
|
|
553
|
+
if (!vault) {
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
556
|
+
isError: true,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const items = [];
|
|
560
|
+
for await (const it of await client.items.list(vault.id)) items.push(it);
|
|
561
|
+
if (items.length === 0) {
|
|
562
|
+
return { content: [{ type: "text", text: `No items in vault "${vault.title}".` }] };
|
|
563
|
+
}
|
|
564
|
+
let text = `# Items in ${vault.title} (${items.length})\n\n`;
|
|
565
|
+
for (const it of items) {
|
|
566
|
+
text += `- **${it.title}** (id: ${it.id}, category: ${it.category})\n`;
|
|
567
|
+
}
|
|
568
|
+
return { content: [{ type: "text", text }] };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (name === "op_search_items") {
|
|
572
|
+
const inst = getInstanceByName(args.instance);
|
|
573
|
+
const client = await getClient(inst);
|
|
574
|
+
const limit = args.limit || 25;
|
|
575
|
+
const q = String(args.query).toLowerCase();
|
|
576
|
+
|
|
577
|
+
let targetVaults = [];
|
|
578
|
+
if (args.vault) {
|
|
579
|
+
const v = await resolveVault(client, args.vault);
|
|
580
|
+
if (!v) {
|
|
581
|
+
return {
|
|
582
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
583
|
+
isError: true,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
targetVaults = [v];
|
|
587
|
+
} else {
|
|
588
|
+
for await (const v of await client.vaults.list()) targetVaults.push(v);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const hits = [];
|
|
592
|
+
for (const v of targetVaults) {
|
|
593
|
+
for await (const it of await client.items.list(v.id)) {
|
|
594
|
+
if ((it.title || "").toLowerCase().includes(q)) {
|
|
595
|
+
hits.push({ ...it, _vaultTitle: v.title });
|
|
596
|
+
if (hits.length >= limit) break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (hits.length >= limit) break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (hits.length === 0) {
|
|
603
|
+
return {
|
|
604
|
+
content: [{ type: "text", text: `No items matched "${args.query}".` }],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
let text = `# Search: "${args.query}" (${hits.length})\n\n`;
|
|
608
|
+
for (const it of hits) {
|
|
609
|
+
text += `- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})\n`;
|
|
610
|
+
}
|
|
611
|
+
return { content: [{ type: "text", text }] };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (name === "op_get_item") {
|
|
615
|
+
const inst = getInstanceByName(args.instance);
|
|
616
|
+
const client = await getClient(inst);
|
|
617
|
+
const vault = await resolveVault(client, args.vault);
|
|
618
|
+
if (!vault) {
|
|
619
|
+
return {
|
|
620
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
621
|
+
isError: true,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
625
|
+
if (!itemRef) {
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: `Item "${args.item}" not found in vault "${vault.title}".` }],
|
|
628
|
+
isError: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const full = await client.items.get(vault.id, itemRef.id);
|
|
632
|
+
const formatted = formatItem(full, !!args.includeConcealed);
|
|
633
|
+
let text = `# ${formatted.title} (${formatted.category})\n\n`;
|
|
634
|
+
text += `- **Vault:** ${vault.title} (${formatted.vaultId})\n`;
|
|
635
|
+
text += `- **Item ID:** ${formatted.id}\n`;
|
|
636
|
+
if (formatted.tags.length > 0) text += `- **Tags:** ${formatted.tags.join(", ")}\n`;
|
|
637
|
+
if (formatted.websites.length > 0) {
|
|
638
|
+
text += `- **Websites:** ${formatted.websites.map((w) => w.url).join(", ")}\n`;
|
|
639
|
+
}
|
|
640
|
+
if (formatted.fields.length > 0) {
|
|
641
|
+
text += `\n## Fields\n\n`;
|
|
642
|
+
for (const f of formatted.fields) {
|
|
643
|
+
const val = f.value === undefined ? "" : f.value;
|
|
644
|
+
text += `- **${f.title}** (${f.type}): ${val}${f.otp ? ` (OTP now: ${f.otp})` : ""}\n`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!args.includeConcealed) {
|
|
648
|
+
text += `\n_Concealed fields are redacted. Call with includeConcealed=true to reveal._`;
|
|
649
|
+
}
|
|
650
|
+
return { content: [{ type: "text", text }] };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (name === "op_get_secret") {
|
|
654
|
+
const inst = getInstanceByName(args.instance);
|
|
655
|
+
const client = await getClient(inst);
|
|
656
|
+
const value = await client.secrets.resolve(args.reference);
|
|
657
|
+
return {
|
|
658
|
+
content: [{ type: "text", text: `Secret for ${args.reference}:\n${value}` }],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (name === "op_generate_password") {
|
|
663
|
+
const type = args.type || "Random";
|
|
664
|
+
let generated;
|
|
665
|
+
if (type === "Pin") {
|
|
666
|
+
generated = sdk.Secrets.generatePassword({
|
|
667
|
+
type: "Pin",
|
|
668
|
+
parameters: { length: args.length || 8 },
|
|
669
|
+
});
|
|
670
|
+
} else if (type === "Memorable") {
|
|
671
|
+
generated = sdk.Secrets.generatePassword({
|
|
672
|
+
type: "Memorable",
|
|
673
|
+
parameters: {
|
|
674
|
+
separatorType: sdk.SeparatorType.Digits,
|
|
675
|
+
capitalize: args.capitalize !== false,
|
|
676
|
+
wordListType: sdk.WordListType.FullWords,
|
|
677
|
+
wordCount: args.wordCount || 5,
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
generated = sdk.Secrets.generatePassword({
|
|
682
|
+
type: "Random",
|
|
683
|
+
parameters: {
|
|
684
|
+
includeDigits: args.includeDigits !== false,
|
|
685
|
+
includeSymbols: args.includeSymbols !== false,
|
|
686
|
+
length: args.length || 20,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
const pwd = typeof generated === "string" ? generated : generated.password || generated;
|
|
691
|
+
return { content: [{ type: "text", text: `Generated (${type}):\n${pwd}` }] };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (name === "op_create_item") {
|
|
695
|
+
const inst = getInstanceByName(args.instance);
|
|
696
|
+
const client = await getClient(inst);
|
|
697
|
+
const vault = await resolveVault(client, args.vault);
|
|
698
|
+
if (!vault) {
|
|
699
|
+
return {
|
|
700
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
701
|
+
isError: true,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const category = sdk.ItemCategory[args.category || "Login"];
|
|
705
|
+
if (!category) {
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: "text", text: `Unknown category "${args.category}".` }],
|
|
708
|
+
isError: true,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const fields = (args.fields || []).map((f) => {
|
|
712
|
+
const ft = sdk.ItemFieldType[f.type];
|
|
713
|
+
if (!ft) throw new Error(`Unknown field type "${f.type}"`);
|
|
714
|
+
const out = {
|
|
715
|
+
id: f.title.toLowerCase().replace(/\s+/g, "_"),
|
|
716
|
+
title: f.title,
|
|
717
|
+
fieldType: ft,
|
|
718
|
+
value: f.value,
|
|
719
|
+
};
|
|
720
|
+
if (f.sectionId) out.sectionId = f.sectionId;
|
|
721
|
+
return out;
|
|
722
|
+
});
|
|
723
|
+
if (args.notes) {
|
|
724
|
+
fields.push({
|
|
725
|
+
id: "notesPlain",
|
|
726
|
+
title: "notesPlain",
|
|
727
|
+
fieldType: sdk.ItemFieldType.Text,
|
|
728
|
+
value: args.notes,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const item = await client.items.create({
|
|
732
|
+
title: args.title,
|
|
733
|
+
category,
|
|
734
|
+
vaultId: vault.id,
|
|
735
|
+
fields,
|
|
736
|
+
tags: args.tags || [],
|
|
737
|
+
websites: (args.websites || []).map((w) => ({
|
|
738
|
+
url: w.url,
|
|
739
|
+
label: w.label || "website",
|
|
740
|
+
autofillBehavior: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
741
|
+
})),
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: `Created item **${item.title}** (id: ${item.id}) in vault "${vault.title}".`,
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (name === "op_delete_item") {
|
|
754
|
+
const inst = getInstanceByName(args.instance);
|
|
755
|
+
const client = await getClient(inst);
|
|
756
|
+
const vault = await resolveVault(client, args.vault);
|
|
757
|
+
if (!vault) {
|
|
758
|
+
return {
|
|
759
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
760
|
+
isError: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
764
|
+
if (!itemRef) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: "text", text: `Item "${args.item}" not found in vault "${vault.title}".` }],
|
|
767
|
+
isError: true,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
await client.items.delete(vault.id, itemRef.id);
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: `Deleted item "${itemRef.title}" from vault "${vault.title}".`,
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (name === "op_create_vault") {
|
|
782
|
+
const inst = getInstanceByName(args.instance);
|
|
783
|
+
const client = await getClient(inst);
|
|
784
|
+
const vault = await client.vaults.create({
|
|
785
|
+
title: args.title,
|
|
786
|
+
description: args.description || "",
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
content: [
|
|
790
|
+
{ type: "text", text: `Created vault **${vault.title}** (id: ${vault.id}).` },
|
|
791
|
+
],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (name === "op_delete_vault") {
|
|
796
|
+
const inst = getInstanceByName(args.instance);
|
|
797
|
+
const client = await getClient(inst);
|
|
798
|
+
const vault = await resolveVault(client, args.vault);
|
|
799
|
+
if (!vault) {
|
|
800
|
+
return {
|
|
801
|
+
content: [{ type: "text", text: `Vault "${args.vault}" not found.` }],
|
|
802
|
+
isError: true,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
await client.vaults.delete(vault.id);
|
|
806
|
+
return { content: [{ type: "text", text: `Deleted vault "${vault.title}".` }] };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
return {
|
|
812
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
813
|
+
isError: true,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
async function main() {
|
|
819
|
+
const transport = new StdioServerTransport();
|
|
820
|
+
await server.connect(transport);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (require.main === module) {
|
|
824
|
+
main().catch((e) => {
|
|
825
|
+
console.error(e);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Export for testing
|
|
831
|
+
if (typeof module !== "undefined") {
|
|
832
|
+
module.exports = {
|
|
833
|
+
loadConfigFile,
|
|
834
|
+
saveConfigFile,
|
|
835
|
+
getInstanceByName,
|
|
836
|
+
redactField,
|
|
837
|
+
formatItem,
|
|
838
|
+
};
|
|
839
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rui.branco/1password-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"1password-mcp": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"setup.js"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"setup": "node setup.js",
|
|
15
|
+
"test": "node --test test/*.test.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"1password",
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code"
|
|
23
|
+
],
|
|
24
|
+
"author": "Rui Branco",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@1password/sdk": "^0.3.1",
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/setup.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const readline = require("readline");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const configDir = path.join(process.env.HOME, ".config/1password-mcp");
|
|
8
|
+
const configPath = path.join(configDir, "config.json");
|
|
9
|
+
|
|
10
|
+
let args = process.argv.slice(2);
|
|
11
|
+
if (args[0] === "setup") args = args.slice(1);
|
|
12
|
+
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(configPath)) {
|
|
16
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveConfig(cfg) {
|
|
23
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
24
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
25
|
+
try {
|
|
26
|
+
fs.chmodSync(configPath, 0o600);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Non-interactive: setup add <name> <token> [description]
|
|
31
|
+
if (args[0] === "add" && args.length >= 3) {
|
|
32
|
+
const [, name, token, ...descArgs] = args;
|
|
33
|
+
const description = descArgs.join(" ").trim();
|
|
34
|
+
const config = loadConfig() || {};
|
|
35
|
+
if (!config.instances) config.instances = [];
|
|
36
|
+
const existing = config.instances.findIndex((i) => i.name === name);
|
|
37
|
+
const instance = { name, token };
|
|
38
|
+
if (description) instance.description = description;
|
|
39
|
+
if (existing >= 0) config.instances[existing] = instance;
|
|
40
|
+
else config.instances.push(instance);
|
|
41
|
+
if (!config.defaultInstance) config.defaultInstance = name;
|
|
42
|
+
saveConfig(config);
|
|
43
|
+
console.log(`Instance "${name}" saved to ${configPath}`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Non-interactive: setup remove <name>
|
|
48
|
+
if (args[0] === "remove" && args.length >= 2) {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
if (!config || !config.instances) {
|
|
51
|
+
console.error("No config found.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
config.instances = config.instances.filter((i) => i.name !== args[1]);
|
|
55
|
+
if (config.defaultInstance === args[1]) {
|
|
56
|
+
config.defaultInstance = config.instances[0]?.name || null;
|
|
57
|
+
}
|
|
58
|
+
saveConfig(config);
|
|
59
|
+
console.log(`Instance "${args[1]}" removed.`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Interactive
|
|
64
|
+
const rl = readline.createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function ask(q) {
|
|
70
|
+
return new Promise((r) => rl.question(q, r));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function setup() {
|
|
74
|
+
console.log("\n=== 1Password MCP Setup ===\n");
|
|
75
|
+
const existing = loadConfig();
|
|
76
|
+
if (existing?.instances?.length) {
|
|
77
|
+
console.log("Existing instances:");
|
|
78
|
+
for (const inst of existing.instances) {
|
|
79
|
+
const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
|
|
80
|
+
const desc = inst.description ? ` - ${inst.description}` : "";
|
|
81
|
+
console.log(` - ${inst.name}${isDefault}${desc}`);
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
const action = await ask("Add another instance? (y/N): ");
|
|
85
|
+
if (action.trim().toLowerCase() !== "y") {
|
|
86
|
+
rl.close();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
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");
|
|
95
|
+
|
|
96
|
+
const name = (await ask("Instance name (e.g., work): ")).trim();
|
|
97
|
+
const token = (await ask("Service account token: ")).trim();
|
|
98
|
+
const description = (await ask("Description (optional): ")).trim();
|
|
99
|
+
|
|
100
|
+
const config = existing || { instances: [] };
|
|
101
|
+
if (!config.instances) config.instances = [];
|
|
102
|
+
const idx = config.instances.findIndex((i) => i.name === name);
|
|
103
|
+
const instance = { name, token };
|
|
104
|
+
if (description) instance.description = description;
|
|
105
|
+
if (idx >= 0) config.instances[idx] = instance;
|
|
106
|
+
else config.instances.push(instance);
|
|
107
|
+
|
|
108
|
+
if (!config.defaultInstance) config.defaultInstance = name;
|
|
109
|
+
else {
|
|
110
|
+
const setDefault = await ask(`Set "${name}" as default? (y/N): `);
|
|
111
|
+
if (setDefault.trim().toLowerCase() === "y") config.defaultInstance = name;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
saveConfig(config);
|
|
115
|
+
console.log(`\nConfig saved to ${configPath} (mode 0600)`);
|
|
116
|
+
console.log("\nAdd to Claude Code with:");
|
|
117
|
+
console.log(" claude mcp add --transport stdio 1password -- node $HOME/WebstormProjects/1password-mcp/index.js");
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setup().catch((e) => {
|
|
122
|
+
console.error("Setup failed:", e.message);
|
|
123
|
+
rl.close();
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|