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

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 +140 -44
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -175,6 +175,17 @@ function invalidateClient(name) {
175
175
  clientCache.delete(name);
176
176
  }
177
177
 
178
+ /**
179
+ * Detect errors that indicate the IPC / desktop-app session is dead so the
180
+ * cached SDK client can be discarded and a fresh one created on retry.
181
+ */
182
+ function isSessionError(e) {
183
+ if (e instanceof sdk.DesktopSessionExpiredError) return true;
184
+ // Belt-and-suspenders: catch IPC / auth wording from the native layer even
185
+ // when the SDK wraps it in a generic Error.
186
+ return /IPC|session.expired|desktop.auth|socket.closed/i.test(e.message);
187
+ }
188
+
178
189
  // ============================================================================
179
190
  // Helpers — vault/item resolution, redaction, formatting
180
191
  // ============================================================================
@@ -522,22 +533,22 @@ const TOOL_DEFINITIONS = [
522
533
  {
523
534
  name: "op_update_item",
524
535
  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.",
536
+ "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
537
  inputSchema: {
527
538
  type: "object",
528
539
  properties: {
529
540
  vault: { type: "string", description: "Vault id or title." },
530
541
  item: { type: "string", description: "Item id or title to update." },
531
542
  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).",
543
+ category: {
544
+ type: "string",
545
+ description:
546
+ "New category (rare). One of Login, SecureNote, Password, ApiCredentials, Database, Server, Email, Identity, CreditCard, etc.",
536
547
  },
537
548
  fields: {
538
549
  type: "array",
539
550
  description:
540
- "Field patches: [{title, value, type?, sectionId?}]. Existing fields are matched by title (case-insensitive) and their values replaced. New fields require `type`.",
551
+ "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
552
  items: {
542
553
  type: "object",
543
554
  properties: {
@@ -549,6 +560,46 @@ const TOOL_DEFINITIONS = [
549
560
  required: ["title", "value"],
550
561
  },
551
562
  },
563
+ tags: {
564
+ type: "array",
565
+ items: { type: "string" },
566
+ description:
567
+ "Replace the tag list. Omit to preserve existing tags; pass [] to clear.",
568
+ },
569
+ websites: {
570
+ type: "array",
571
+ description:
572
+ "Replace the website list. Each website is {url, label?, autofillBehavior?}. Omit to preserve existing websites.",
573
+ items: {
574
+ type: "object",
575
+ properties: {
576
+ url: { type: "string" },
577
+ label: { type: "string" },
578
+ autofillBehavior: {
579
+ type: "string",
580
+ description: "'AnywhereOnWebsite' (default) or 'Never'.",
581
+ },
582
+ },
583
+ required: ["url"],
584
+ },
585
+ },
586
+ sections: {
587
+ type: "array",
588
+ description: "Replace the section list. Each section is {title, id?}.",
589
+ items: {
590
+ type: "object",
591
+ properties: {
592
+ title: { type: "string" },
593
+ id: { type: "string" },
594
+ },
595
+ required: ["title"],
596
+ },
597
+ },
598
+ notes: {
599
+ type: "string",
600
+ description:
601
+ "Replace the notesPlain field. Omit to preserve existing notes.",
602
+ },
552
603
  instance: {
553
604
  type: "string",
554
605
  description: "Instance name. Uses default if omitted.",
@@ -952,44 +1003,81 @@ async function updateItemHandler(args) {
952
1003
  const current = await client.items.get(vault.id, itemRef.id);
953
1004
  const updated = { ...current };
954
1005
 
1006
+ // --- title (rename) ---
955
1007
  if (args.title) updated.title = args.title;
1008
+
1009
+ // --- category (rarely changed, but the SDK allows it) ---
1010
+ if (args.category) {
1011
+ const cat = sdk.ItemCategory[args.category];
1012
+ if (!cat) throw new Error(`Unknown category "${args.category}".`);
1013
+ updated.category = cat;
1014
+ }
1015
+
1016
+ // --- tags (replace when passed; preserved when omitted) ---
956
1017
  if (args.tags) updated.tags = args.tags;
957
1018
 
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
- );
1019
+ // --- websites (replace when passed; preserved when omitted) ---
1020
+ if (Array.isArray(args.websites)) {
1021
+ updated.websites = args.websites.map((w) => ({
1022
+ url: w.url,
1023
+ label: w.label || "website",
1024
+ autofillBehavior:
1025
+ w.autofillBehavior === "Never"
1026
+ ? sdk.AutofillBehavior.Never
1027
+ : sdk.AutofillBehavior.AnywhereOnWebsite,
1028
+ }));
1029
+ }
963
1030
 
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
- });
1031
+ // --- sections (replace when passed; preserved when omitted) ---
1032
+ if (Array.isArray(args.sections)) {
1033
+ updated.sections = args.sections.map((s) => ({
1034
+ id: s.id || s.title.toLowerCase().replace(/\s+/g, "_"),
1035
+ title: s.title,
1036
+ }));
1037
+ }
971
1038
 
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
- });
1039
+ // --- fields (patch by title, add new ones with explicit type) ---
1040
+ const nextFields = [...(current.fields || [])];
1041
+ const fieldIndexByTitle = new Map(
1042
+ nextFields.map((f, idx) => [(f.title || "").toLowerCase(), idx]),
1043
+ );
1044
+
1045
+ function upsertField({ title, value, type, sectionId }) {
1046
+ const key = title.toLowerCase();
1047
+ const existingIdx = fieldIndexByTitle.get(key);
1048
+ if (existingIdx !== undefined) {
1049
+ nextFields[existingIdx] = { ...nextFields[existingIdx], value };
1050
+ return;
989
1051
  }
990
- updated.fields = [...patchedExisting, ...additions];
1052
+ if (!type) {
1053
+ throw new Error(
1054
+ `Field "${title}" doesn't exist on the item — pass a 'type' to add it as a new field.`,
1055
+ );
1056
+ }
1057
+ const ft = sdk.ItemFieldType[type];
1058
+ if (!ft) throw new Error(`Unknown field type "${type}".`);
1059
+ const added = {
1060
+ id: title.toLowerCase().replace(/\s+/g, "_"),
1061
+ title,
1062
+ fieldType: ft,
1063
+ value,
1064
+ };
1065
+ if (sectionId) added.sectionId = sectionId;
1066
+ fieldIndexByTitle.set(key, nextFields.length);
1067
+ nextFields.push(added);
1068
+ }
1069
+
1070
+ if (Array.isArray(args.fields)) {
1071
+ for (const patch of args.fields) upsertField(patch);
1072
+ }
1073
+
1074
+ // --- notes: stored as a 'notesPlain' Text field, so patch it the same way ---
1075
+ if (args.notes !== undefined) {
1076
+ upsertField({ title: "notesPlain", value: args.notes, type: "Text" });
991
1077
  }
992
1078
 
1079
+ updated.fields = nextFields;
1080
+
993
1081
  const result = await client.items.put(updated);
994
1082
  return `Updated item "${result.title}" (id: ${result.id}) in vault "${vault.title}".`;
995
1083
  }
@@ -1140,14 +1228,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1140
1228
  isError: true,
1141
1229
  };
1142
1230
  }
1143
- try {
1144
- const result = await handler(args || {});
1145
- return { content: [{ type: "text", text: String(result) }] };
1146
- } catch (e) {
1147
- return {
1148
- content: [{ type: "text", text: `Error: ${e.message}` }],
1149
- isError: true,
1150
- };
1231
+ for (let attempt = 0; attempt < 2; attempt++) {
1232
+ try {
1233
+ const result = await handler(args || {});
1234
+ return { content: [{ type: "text", text: String(result) }] };
1235
+ } catch (e) {
1236
+ if (attempt === 0 && isSessionError(e)) {
1237
+ // Stale IPC connection — purge cached clients and retry once so the
1238
+ // next getClient() call creates a fresh session.
1239
+ clientCache.clear();
1240
+ continue;
1241
+ }
1242
+ return {
1243
+ content: [{ type: "text", text: `Error: ${e.message}` }],
1244
+ isError: true,
1245
+ };
1246
+ }
1151
1247
  }
1152
1248
  });
1153
1249
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/1password-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {