@rui.branco/1password-mcp 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +207 -17
  2. 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,65 @@ const TOOL_DEFINITIONS = [
514
519
  required: ["vault", "title"],
515
520
  },
516
521
  },
522
+ {
523
+ name: "op_update_item",
524
+ description:
525
+ "Update an existing item. Patch fields by title — e.g. to change the password on 'GitHub', pass fields: [{title: 'password', value: 'new-pwd'}]. To add a new field, include its type. Omitting fields leaves them untouched. Can also rename the item (title) or replace its tags.",
526
+ inputSchema: {
527
+ type: "object",
528
+ properties: {
529
+ vault: { type: "string", description: "Vault id or title." },
530
+ item: { type: "string", description: "Item id or title to update." },
531
+ title: { type: "string", description: "New title (optional)." },
532
+ tags: {
533
+ type: "array",
534
+ items: { type: "string" },
535
+ description: "Replace the item's tag list (optional).",
536
+ },
537
+ fields: {
538
+ type: "array",
539
+ description:
540
+ "Field patches: [{title, value, type?, sectionId?}]. Existing fields are matched by title (case-insensitive) and their values replaced. New fields require `type`.",
541
+ items: {
542
+ type: "object",
543
+ properties: {
544
+ title: { type: "string" },
545
+ value: { type: "string" },
546
+ type: { type: "string" },
547
+ sectionId: { type: "string" },
548
+ },
549
+ required: ["title", "value"],
550
+ },
551
+ },
552
+ instance: {
553
+ type: "string",
554
+ description: "Instance name. Uses default if omitted.",
555
+ },
556
+ },
557
+ required: ["vault", "item"],
558
+ },
559
+ },
560
+ {
561
+ name: "op_update_vault",
562
+ description:
563
+ "Update a vault's title and/or description. Pass at least one of them.",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {
567
+ vault: { type: "string", description: "Vault id or title." },
568
+ title: { type: "string", description: "New title (optional)." },
569
+ description: {
570
+ type: "string",
571
+ description: "New description (optional).",
572
+ },
573
+ instance: {
574
+ type: "string",
575
+ description: "Instance name. Uses default if omitted.",
576
+ },
577
+ },
578
+ required: ["vault"],
579
+ },
580
+ },
517
581
  {
518
582
  name: "op_delete_item",
519
583
  description: "Delete an item by vault + item reference.",
@@ -709,15 +773,19 @@ async function listItemsHandler(args) {
709
773
  return `# Items in ${vault.title} (${items.length})\n\n${lines.join("\n")}`;
710
774
  }
711
775
 
712
- async function searchItemsHandler(args) {
713
- const client = await getClient(getInstanceByName(args.instance));
714
- const limit = args.limit || 25;
715
- const q = String(args.query).toLowerCase();
776
+ /**
777
+ * Core search implementation — pulled out of the handler so it can be unit
778
+ * tested with a fake client. Accepts an already-resolved client.
779
+ */
780
+ async function searchItems(client, { query, vault, reveal, limit }) {
781
+ const wantReveal = !!reveal;
782
+ const max = limit || (wantReveal ? 5 : 25);
783
+ const q = String(query).toLowerCase();
716
784
 
717
785
  let targetVaults;
718
- if (args.vault) {
719
- const v = await resolveVault(client, args.vault);
720
- if (!v) throw new Error(`Vault "${args.vault}" not found.`);
786
+ if (vault) {
787
+ const v = await resolveVault(client, vault);
788
+ if (!v) throw new Error(`Vault "${vault}" not found.`);
721
789
  targetVaults = [v];
722
790
  } else {
723
791
  targetVaults = await collect(await client.vaults.list());
@@ -727,19 +795,66 @@ async function searchItemsHandler(args) {
727
795
  for (const v of targetVaults) {
728
796
  for await (const it of await client.items.list(v.id)) {
729
797
  if ((it.title || "").toLowerCase().includes(q)) {
730
- hits.push({ ...it, _vaultTitle: v.title });
731
- if (hits.length >= limit) break;
798
+ hits.push({ ...it, _vaultId: v.id, _vaultTitle: v.title });
799
+ if (hits.length >= max) break;
732
800
  }
733
801
  }
734
- if (hits.length >= limit) break;
802
+ if (hits.length >= max) break;
803
+ }
804
+
805
+ if (hits.length === 0) return `No items matched "${query}".`;
806
+
807
+ if (!wantReveal) {
808
+ const lines = hits.map(
809
+ (it) =>
810
+ `- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})`,
811
+ );
812
+ return `# Search: "${query}" (${hits.length})\n\n${lines.join("\n")}`;
735
813
  }
736
814
 
737
- if (hits.length === 0) return `No items matched "${args.query}".`;
738
- const lines = hits.map(
739
- (it) =>
740
- `- **${it.title}** (id: ${it.id}, vault: ${it._vaultTitle}, category: ${it.category})`,
815
+ // reveal=true: fetch each hit's full item in parallel and render every
816
+ // field (including concealed values). One-shot path for "give me the X
817
+ // credential" style queries.
818
+ const full = await Promise.all(
819
+ hits.map(async (it) => {
820
+ try {
821
+ const item = await client.items.get(it._vaultId, it.id);
822
+ return { item, _vaultTitle: it._vaultTitle, _error: null };
823
+ } catch (e) {
824
+ return { item: it, _vaultTitle: it._vaultTitle, _error: e.message };
825
+ }
826
+ }),
741
827
  );
742
- return `# Search: "${args.query}" (${hits.length})\n\n${lines.join("\n")}`;
828
+
829
+ const sections = full.map((entry) => {
830
+ if (entry._error) {
831
+ return `## ${entry.item.title}\n\n_Failed to fetch: ${entry._error}_`;
832
+ }
833
+ const it = entry.item;
834
+ const fields = (it.fields || [])
835
+ .map((f) => redactField(f, true))
836
+ .filter((f) => f.value !== undefined && f.value !== "");
837
+ const fieldLines = fields.length
838
+ ? fields
839
+ .map((f) => {
840
+ const otp = f.otp ? ` (OTP now: ${f.otp})` : "";
841
+ return `- **${f.title}** (${f.type}): ${f.value}${otp}`;
842
+ })
843
+ .join("\n")
844
+ : "_(no fields)_";
845
+ const websites = (it.websites || []).map((w) => w.url).filter(Boolean);
846
+ const websiteLine = websites.length
847
+ ? `- **URLs:** ${websites.join(", ")}\n`
848
+ : "";
849
+ return `## ${it.title} (${it.category})\n\n- **Vault:** ${entry._vaultTitle}\n- **Item ID:** ${it.id}\n${websiteLine}\n${fieldLines}`;
850
+ });
851
+
852
+ return `# Search (revealed): "${query}" (${hits.length})\n\n${sections.join("\n\n")}`;
853
+ }
854
+
855
+ async function searchItemsHandler(args) {
856
+ const client = await getClient(getInstanceByName(args.instance));
857
+ return searchItems(client, args);
743
858
  }
744
859
 
745
860
  async function getItemHandler(args) {
@@ -823,6 +938,76 @@ function generatePasswordHandler(args) {
823
938
  return `Generated (${type}):\n${pwd}`;
824
939
  }
825
940
 
941
+ async function updateItemHandler(args) {
942
+ const client = await getClient(getInstanceByName(args.instance));
943
+ const vault = await resolveVault(client, args.vault);
944
+ if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
945
+ const itemRef = await resolveItem(client, vault, args.item);
946
+ if (!itemRef) {
947
+ throw new Error(
948
+ `Item "${args.item}" not found in vault "${vault.title}".`,
949
+ );
950
+ }
951
+
952
+ const current = await client.items.get(vault.id, itemRef.id);
953
+ const updated = { ...current };
954
+
955
+ if (args.title) updated.title = args.title;
956
+ if (args.tags) updated.tags = args.tags;
957
+
958
+ if (Array.isArray(args.fields) && args.fields.length > 0) {
959
+ const existing = current.fields || [];
960
+ const patchesByTitle = new Map(
961
+ args.fields.map((p) => [p.title.toLowerCase(), p]),
962
+ );
963
+
964
+ // Patch existing fields in place, preserving id/type/sectionId
965
+ const patchedExisting = existing.map((f) => {
966
+ const patch = patchesByTitle.get((f.title || "").toLowerCase());
967
+ if (!patch) return f;
968
+ patchesByTitle.delete((f.title || "").toLowerCase());
969
+ return { ...f, value: patch.value };
970
+ });
971
+
972
+ // Remaining patches = new fields to add (must specify type)
973
+ const additions = [];
974
+ for (const patch of patchesByTitle.values()) {
975
+ if (!patch.type) {
976
+ throw new Error(
977
+ `Field "${patch.title}" doesn't exist on the item — pass a 'type' to add it as a new field.`,
978
+ );
979
+ }
980
+ const ft = sdk.ItemFieldType[patch.type];
981
+ if (!ft) throw new Error(`Unknown field type "${patch.type}".`);
982
+ additions.push({
983
+ id: patch.title.toLowerCase().replace(/\s+/g, "_"),
984
+ title: patch.title,
985
+ fieldType: ft,
986
+ value: patch.value,
987
+ sectionId: patch.sectionId,
988
+ });
989
+ }
990
+ updated.fields = [...patchedExisting, ...additions];
991
+ }
992
+
993
+ const result = await client.items.put(updated);
994
+ return `Updated item "${result.title}" (id: ${result.id}) in vault "${vault.title}".`;
995
+ }
996
+
997
+ async function updateVaultHandler(args) {
998
+ const client = await getClient(getInstanceByName(args.instance));
999
+ const vault = await resolveVault(client, args.vault);
1000
+ if (!vault) throw new Error(`Vault "${args.vault}" not found.`);
1001
+ if (!args.title && args.description === undefined) {
1002
+ throw new Error("Provide at least one of title or description.");
1003
+ }
1004
+ await client.vaults.update(vault.id, {
1005
+ title: args.title || vault.title,
1006
+ description: args.description !== undefined ? args.description : vault.description || "",
1007
+ });
1008
+ return `Updated vault "${args.title || vault.title}" (id: ${vault.id}).`;
1009
+ }
1010
+
826
1011
  async function createItemHandler(args) {
827
1012
  const client = await getClient(getInstanceByName(args.instance));
828
1013
  const vault = await resolveVault(client, args.vault);
@@ -909,8 +1094,10 @@ const HANDLERS = {
909
1094
  op_get_secret: getSecretHandler,
910
1095
  op_generate_password: generatePasswordHandler,
911
1096
  op_create_item: createItemHandler,
1097
+ op_update_item: updateItemHandler,
912
1098
  op_delete_item: deleteItemHandler,
913
1099
  op_create_vault: createVaultHandler,
1100
+ op_update_vault: updateVaultHandler,
914
1101
  op_delete_vault: deleteVaultHandler,
915
1102
  };
916
1103
 
@@ -996,6 +1183,7 @@ if (typeof module !== "undefined") {
996
1183
  resolveItem,
997
1184
  redactField,
998
1185
  formatItem,
1186
+ searchItems,
999
1187
  // handler map + tool list (for drift guard tests)
1000
1188
  TOOL_DEFINITIONS,
1001
1189
  HANDLERS,
@@ -1006,5 +1194,7 @@ if (typeof module !== "undefined") {
1006
1194
  searchItemsHandler,
1007
1195
  getItemHandler,
1008
1196
  generatePasswordHandler,
1197
+ updateItemHandler,
1198
+ updateVaultHandler,
1009
1199
  };
1010
1200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/1password-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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": {