@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.
Files changed (2) hide show
  1. package/index.js +284 -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,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
- 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();
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 (args.vault) {
719
- const v = await resolveVault(client, args.vault);
720
- if (!v) throw new Error(`Vault "${args.vault}" not found.`);
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 >= limit) break;
838
+ hits.push({ ...it, _vaultId: v.id, _vaultTitle: v.title });
839
+ if (hits.length >= max) break;
732
840
  }
733
841
  }
734
- if (hits.length >= limit) break;
842
+ if (hits.length >= max) break;
735
843
  }
736
844
 
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})`,
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
- return `# Search: "${args.query}" (${hits.length})\n\n${lines.join("\n")}`;
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",
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": {