@rui.branco/1password-mcp 0.1.3 → 0.1.5
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 +284 -17
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -361,7 +361,7 @@ const TOOL_DEFINITIONS = [
|
|
|
361
361
|
{
|
|
362
362
|
name: "op_search_items",
|
|
363
363
|
description:
|
|
364
|
-
"Search items by title across vaults. If `vault` is omitted, searches every vault the account can see.",
|
|
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
365
|
inputSchema: {
|
|
366
366
|
type: "object",
|
|
367
367
|
properties: {
|
|
@@ -375,7 +375,12 @@ const TOOL_DEFINITIONS = [
|
|
|
375
375
|
},
|
|
376
376
|
limit: {
|
|
377
377
|
type: "number",
|
|
378
|
-
description: "Max results (default 25).",
|
|
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.",
|
|
379
384
|
},
|
|
380
385
|
instance: {
|
|
381
386
|
type: "string",
|
|
@@ -514,6 +519,105 @@ const TOOL_DEFINITIONS = [
|
|
|
514
519
|
required: ["vault", "title"],
|
|
515
520
|
},
|
|
516
521
|
},
|
|
522
|
+
{
|
|
523
|
+
name: "op_update_item",
|
|
524
|
+
description:
|
|
525
|
+
"Update any part of an existing item — title, category, fields, tags, websites, sections, or notes. Omitted parameters are preserved. Fields are patched by title (existing fields match case-insensitively; new fields require `type`). Tags, websites, and sections are REPLACED when passed.",
|
|
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
|
+
category: {
|
|
533
|
+
type: "string",
|
|
534
|
+
description:
|
|
535
|
+
"New category (rare). One of Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
|
|
536
|
+
},
|
|
537
|
+
fields: {
|
|
538
|
+
type: "array",
|
|
539
|
+
description:
|
|
540
|
+
"Field patches: [{title, value, type?, sectionId?}]. Existing fields matched by title have their values replaced. New fields require `type` (Text | Concealed | Totp | Email | Url | Phone).",
|
|
541
|
+
items: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
title: { type: "string" },
|
|
545
|
+
value: { type: "string" },
|
|
546
|
+
type: { type: "string" },
|
|
547
|
+
sectionId: { type: "string" },
|
|
548
|
+
},
|
|
549
|
+
required: ["title", "value"],
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
tags: {
|
|
553
|
+
type: "array",
|
|
554
|
+
items: { type: "string" },
|
|
555
|
+
description:
|
|
556
|
+
"Replace the tag list. Omit to preserve existing tags; pass [] to clear.",
|
|
557
|
+
},
|
|
558
|
+
websites: {
|
|
559
|
+
type: "array",
|
|
560
|
+
description:
|
|
561
|
+
"Replace the website list. Each website is {url, label?, autofillBehavior?}. Omit to preserve existing websites.",
|
|
562
|
+
items: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
url: { type: "string" },
|
|
566
|
+
label: { type: "string" },
|
|
567
|
+
autofillBehavior: {
|
|
568
|
+
type: "string",
|
|
569
|
+
description: "'AnywhereOnWebsite' (default) or 'Never'.",
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
required: ["url"],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
sections: {
|
|
576
|
+
type: "array",
|
|
577
|
+
description: "Replace the section list. Each section is {title, id?}.",
|
|
578
|
+
items: {
|
|
579
|
+
type: "object",
|
|
580
|
+
properties: {
|
|
581
|
+
title: { type: "string" },
|
|
582
|
+
id: { type: "string" },
|
|
583
|
+
},
|
|
584
|
+
required: ["title"],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
notes: {
|
|
588
|
+
type: "string",
|
|
589
|
+
description:
|
|
590
|
+
"Replace the notesPlain field. Omit to preserve existing notes.",
|
|
591
|
+
},
|
|
592
|
+
instance: {
|
|
593
|
+
type: "string",
|
|
594
|
+
description: "Instance name. Uses default if omitted.",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
required: ["vault", "item"],
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "op_update_vault",
|
|
602
|
+
description:
|
|
603
|
+
"Update a vault's title and/or description. Pass at least one of them.",
|
|
604
|
+
inputSchema: {
|
|
605
|
+
type: "object",
|
|
606
|
+
properties: {
|
|
607
|
+
vault: { type: "string", description: "Vault id or title." },
|
|
608
|
+
title: { type: "string", description: "New title (optional)." },
|
|
609
|
+
description: {
|
|
610
|
+
type: "string",
|
|
611
|
+
description: "New description (optional).",
|
|
612
|
+
},
|
|
613
|
+
instance: {
|
|
614
|
+
type: "string",
|
|
615
|
+
description: "Instance name. Uses default if omitted.",
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
required: ["vault"],
|
|
619
|
+
},
|
|
620
|
+
},
|
|
517
621
|
{
|
|
518
622
|
name: "op_delete_item",
|
|
519
623
|
description: "Delete an item by vault + item reference.",
|
|
@@ -709,15 +813,19 @@ async function listItemsHandler(args) {
|
|
|
709
813
|
return `# Items in ${vault.title} (${items.length})\n\n${lines.join("\n")}`;
|
|
710
814
|
}
|
|
711
815
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Core search implementation — pulled out of the handler so it can be unit
|
|
818
|
+
* tested with a fake client. Accepts an already-resolved client.
|
|
819
|
+
*/
|
|
820
|
+
async function searchItems(client, { query, vault, reveal, limit }) {
|
|
821
|
+
const wantReveal = !!reveal;
|
|
822
|
+
const max = limit || (wantReveal ? 5 : 25);
|
|
823
|
+
const q = String(query).toLowerCase();
|
|
716
824
|
|
|
717
825
|
let targetVaults;
|
|
718
|
-
if (
|
|
719
|
-
const v = await resolveVault(client,
|
|
720
|
-
if (!v) throw new Error(`Vault "${
|
|
826
|
+
if (vault) {
|
|
827
|
+
const v = await resolveVault(client, vault);
|
|
828
|
+
if (!v) throw new Error(`Vault "${vault}" not found.`);
|
|
721
829
|
targetVaults = [v];
|
|
722
830
|
} else {
|
|
723
831
|
targetVaults = await collect(await client.vaults.list());
|
|
@@ -727,19 +835,66 @@ async function searchItemsHandler(args) {
|
|
|
727
835
|
for (const v of targetVaults) {
|
|
728
836
|
for await (const it of await client.items.list(v.id)) {
|
|
729
837
|
if ((it.title || "").toLowerCase().includes(q)) {
|
|
730
|
-
hits.push({ ...it, _vaultTitle: v.title });
|
|
731
|
-
if (hits.length >=
|
|
838
|
+
hits.push({ ...it, _vaultId: v.id, _vaultTitle: v.title });
|
|
839
|
+
if (hits.length >= max) break;
|
|
732
840
|
}
|
|
733
841
|
}
|
|
734
|
-
if (hits.length >=
|
|
842
|
+
if (hits.length >= max) break;
|
|
735
843
|
}
|
|
736
844
|
|
|
737
|
-
if (hits.length === 0) return `No items matched "${
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
845
|
+
if (hits.length === 0) return `No items matched "${query}".`;
|
|
846
|
+
|
|
847
|
+
if (!wantReveal) {
|
|
848
|
+
const lines = hits.map(
|
|
849
|
+
(it) =>
|
|
850
|
+
`- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})`,
|
|
851
|
+
);
|
|
852
|
+
return `# Search: "${query}" (${hits.length})\n\n${lines.join("\n")}`;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// reveal=true: fetch each hit's full item in parallel and render every
|
|
856
|
+
// field (including concealed values). One-shot path for "give me the X
|
|
857
|
+
// credential" style queries.
|
|
858
|
+
const full = await Promise.all(
|
|
859
|
+
hits.map(async (it) => {
|
|
860
|
+
try {
|
|
861
|
+
const item = await client.items.get(it._vaultId, it.id);
|
|
862
|
+
return { item, _vaultTitle: it._vaultTitle, _error: null };
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return { item: it, _vaultTitle: it._vaultTitle, _error: e.message };
|
|
865
|
+
}
|
|
866
|
+
}),
|
|
741
867
|
);
|
|
742
|
-
|
|
868
|
+
|
|
869
|
+
const sections = full.map((entry) => {
|
|
870
|
+
if (entry._error) {
|
|
871
|
+
return `## ${entry.item.title}\n\n_Failed to fetch: ${entry._error}_`;
|
|
872
|
+
}
|
|
873
|
+
const it = entry.item;
|
|
874
|
+
const fields = (it.fields || [])
|
|
875
|
+
.map((f) => redactField(f, true))
|
|
876
|
+
.filter((f) => f.value !== undefined && f.value !== "");
|
|
877
|
+
const fieldLines = fields.length
|
|
878
|
+
? fields
|
|
879
|
+
.map((f) => {
|
|
880
|
+
const otp = f.otp ? ` (OTP now: ${f.otp})` : "";
|
|
881
|
+
return `- **${f.title}** (${f.type}): ${f.value}${otp}`;
|
|
882
|
+
})
|
|
883
|
+
.join("\n")
|
|
884
|
+
: "_(no fields)_";
|
|
885
|
+
const websites = (it.websites || []).map((w) => w.url).filter(Boolean);
|
|
886
|
+
const websiteLine = websites.length
|
|
887
|
+
? `- **URLs:** ${websites.join(", ")}\n`
|
|
888
|
+
: "";
|
|
889
|
+
return `## ${it.title} (${it.category})\n\n- **Vault:** ${entry._vaultTitle}\n- **Item ID:** ${it.id}\n${websiteLine}\n${fieldLines}`;
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return `# Search (revealed): "${query}" (${hits.length})\n\n${sections.join("\n\n")}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function searchItemsHandler(args) {
|
|
896
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
897
|
+
return searchItems(client, args);
|
|
743
898
|
}
|
|
744
899
|
|
|
745
900
|
async function getItemHandler(args) {
|
|
@@ -823,6 +978,113 @@ function generatePasswordHandler(args) {
|
|
|
823
978
|
return `Generated (${type}):\n${pwd}`;
|
|
824
979
|
}
|
|
825
980
|
|
|
981
|
+
async function updateItemHandler(args) {
|
|
982
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
983
|
+
const vault = await resolveVault(client, args.vault);
|
|
984
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
985
|
+
const itemRef = await resolveItem(client, vault, args.item);
|
|
986
|
+
if (!itemRef) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`Item "${args.item}" not found in vault "${vault.title}".`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const current = await client.items.get(vault.id, itemRef.id);
|
|
993
|
+
const updated = { ...current };
|
|
994
|
+
|
|
995
|
+
// --- title (rename) ---
|
|
996
|
+
if (args.title) updated.title = args.title;
|
|
997
|
+
|
|
998
|
+
// --- category (rarely changed, but the SDK allows it) ---
|
|
999
|
+
if (args.category) {
|
|
1000
|
+
const cat = sdk.ItemCategory[args.category];
|
|
1001
|
+
if (!cat) throw new Error(`Unknown category "${args.category}".`);
|
|
1002
|
+
updated.category = cat;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// --- tags (replace when passed; preserved when omitted) ---
|
|
1006
|
+
if (args.tags) updated.tags = args.tags;
|
|
1007
|
+
|
|
1008
|
+
// --- websites (replace when passed; preserved when omitted) ---
|
|
1009
|
+
if (Array.isArray(args.websites)) {
|
|
1010
|
+
updated.websites = args.websites.map((w) => ({
|
|
1011
|
+
url: w.url,
|
|
1012
|
+
label: w.label || "website",
|
|
1013
|
+
autofillBehavior:
|
|
1014
|
+
w.autofillBehavior === "Never"
|
|
1015
|
+
? sdk.AutofillBehavior.Never
|
|
1016
|
+
: sdk.AutofillBehavior.AnywhereOnWebsite,
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// --- sections (replace when passed; preserved when omitted) ---
|
|
1021
|
+
if (Array.isArray(args.sections)) {
|
|
1022
|
+
updated.sections = args.sections.map((s) => ({
|
|
1023
|
+
id: s.id || s.title.toLowerCase().replace(/\s+/g, "_"),
|
|
1024
|
+
title: s.title,
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// --- fields (patch by title, add new ones with explicit type) ---
|
|
1029
|
+
const nextFields = [...(current.fields || [])];
|
|
1030
|
+
const fieldIndexByTitle = new Map(
|
|
1031
|
+
nextFields.map((f, idx) => [(f.title || "").toLowerCase(), idx]),
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
function upsertField({ title, value, type, sectionId }) {
|
|
1035
|
+
const key = title.toLowerCase();
|
|
1036
|
+
const existingIdx = fieldIndexByTitle.get(key);
|
|
1037
|
+
if (existingIdx !== undefined) {
|
|
1038
|
+
nextFields[existingIdx] = { ...nextFields[existingIdx], value };
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (!type) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Field "${title}" doesn't exist on the item — pass a 'type' to add it as a new field.`,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const ft = sdk.ItemFieldType[type];
|
|
1047
|
+
if (!ft) throw new Error(`Unknown field type "${type}".`);
|
|
1048
|
+
const added = {
|
|
1049
|
+
id: title.toLowerCase().replace(/\s+/g, "_"),
|
|
1050
|
+
title,
|
|
1051
|
+
fieldType: ft,
|
|
1052
|
+
value,
|
|
1053
|
+
};
|
|
1054
|
+
if (sectionId) added.sectionId = sectionId;
|
|
1055
|
+
fieldIndexByTitle.set(key, nextFields.length);
|
|
1056
|
+
nextFields.push(added);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (Array.isArray(args.fields)) {
|
|
1060
|
+
for (const patch of args.fields) upsertField(patch);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// --- notes: stored as a 'notesPlain' Text field, so patch it the same way ---
|
|
1064
|
+
if (args.notes !== undefined) {
|
|
1065
|
+
upsertField({ title: "notesPlain", value: args.notes, type: "Text" });
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
updated.fields = nextFields;
|
|
1069
|
+
|
|
1070
|
+
const result = await client.items.put(updated);
|
|
1071
|
+
return `Updated item "${result.title}" (id: ${result.id}) in vault "${vault.title}".`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async function updateVaultHandler(args) {
|
|
1075
|
+
const client = await getClient(getInstanceByName(args.instance));
|
|
1076
|
+
const vault = await resolveVault(client, args.vault);
|
|
1077
|
+
if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
|
|
1078
|
+
if (!args.title && args.description === undefined) {
|
|
1079
|
+
throw new Error("Provide at least one of title or description.");
|
|
1080
|
+
}
|
|
1081
|
+
await client.vaults.update(vault.id, {
|
|
1082
|
+
title: args.title || vault.title,
|
|
1083
|
+
description: args.description !== undefined ? args.description : vault.description || "",
|
|
1084
|
+
});
|
|
1085
|
+
return `Updated vault "${args.title || vault.title}" (id: ${vault.id}).`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
826
1088
|
async function createItemHandler(args) {
|
|
827
1089
|
const client = await getClient(getInstanceByName(args.instance));
|
|
828
1090
|
const vault = await resolveVault(client, args.vault);
|
|
@@ -909,8 +1171,10 @@ const HANDLERS = {
|
|
|
909
1171
|
op_get_secret: getSecretHandler,
|
|
910
1172
|
op_generate_password: generatePasswordHandler,
|
|
911
1173
|
op_create_item: createItemHandler,
|
|
1174
|
+
op_update_item: updateItemHandler,
|
|
912
1175
|
op_delete_item: deleteItemHandler,
|
|
913
1176
|
op_create_vault: createVaultHandler,
|
|
1177
|
+
op_update_vault: updateVaultHandler,
|
|
914
1178
|
op_delete_vault: deleteVaultHandler,
|
|
915
1179
|
};
|
|
916
1180
|
|
|
@@ -996,6 +1260,7 @@ if (typeof module !== "undefined") {
|
|
|
996
1260
|
resolveItem,
|
|
997
1261
|
redactField,
|
|
998
1262
|
formatItem,
|
|
1263
|
+
searchItems,
|
|
999
1264
|
// handler map + tool list (for drift guard tests)
|
|
1000
1265
|
TOOL_DEFINITIONS,
|
|
1001
1266
|
HANDLERS,
|
|
@@ -1006,5 +1271,7 @@ if (typeof module !== "undefined") {
|
|
|
1006
1271
|
searchItemsHandler,
|
|
1007
1272
|
getItemHandler,
|
|
1008
1273
|
generatePasswordHandler,
|
|
1274
|
+
updateItemHandler,
|
|
1275
|
+
updateVaultHandler,
|
|
1009
1276
|
};
|
|
1010
1277
|
}
|
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.5",
|
|
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": {
|