Skip to content

Commit 67693ff

Browse files
Merge pull request #11 from CakeRepository/copilot/fix-comments-in-review-thread
Align tool schema docs with supported item_get and note_create behavior
2 parents 8e1f886 + e0025b3 commit 67693ff

8 files changed

Lines changed: 876 additions & 3 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ A community-built [Model Context Protocol (MCP)](https://modelcontextprotocol.io
1212

1313
## Features
1414

15-
### Tools (8)
15+
### Tools (13)
1616

1717
| Tool | Description |
1818
|------|-------------|
1919
| `vault_list` | List all accessible vaults |
2020
| `item_lookup` | Search items by title in a vault |
21+
| `item_list` | List all items in a vault (id, title, category, tags, updatedAt) |
22+
| `item_get` | Retrieve a full item (title, category, tags, notes, fields); conceals secret values unless `reveal` is true |
23+
| `item_edit` | Edit an item's title, notes, tags, URL, and fields (upsert/remove); empty `notes` clears notes |
2124
| `item_delete` | Delete an item from a vault |
25+
| `item_archive` | Archive an item (move to archive instead of permanently deleting) |
26+
| `note_create` | Create a Secure Note item with optional tags and custom fields |
2227
| `password_create` | Create a new password/login item |
2328
| `password_read` | Retrieve a password via secret reference (`op://vault/item/field`) or vault/item ID |
2429
| `password_update` | Rotate/update an existing password |

src/tools/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
66
import { registerVaultList } from "./vault-list.js";
77
import { registerItemLookup } from "./item-lookup.js";
88
import { registerItemDelete } from "./item-delete.js";
9+
import { registerItemGet } from "./item-get.js";
10+
import { registerItemEdit } from "./item-edit.js";
11+
import { registerItemList } from "./item-list.js";
12+
import { registerItemArchive } from "./item-archive.js";
13+
import { registerNoteCreate } from "./note-create.js";
914
import { registerPasswordCreate } from "./password-create.js";
1015
import { registerPasswordRead } from "./password-read.js";
1116
import { registerPasswordUpdate } from "./password-update.js";
@@ -17,6 +22,11 @@ export function registerAllTools(server: McpServer): void {
1722
registerVaultList(server);
1823
registerItemLookup(server);
1924
registerItemDelete(server);
25+
registerItemGet(server);
26+
registerItemEdit(server);
27+
registerItemList(server);
28+
registerItemArchive(server);
29+
registerNoteCreate(server);
2030
registerPasswordCreate(server);
2131
registerPasswordRead(server);
2232
registerPasswordUpdate(server);

src/tools/item-archive.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* item_archive — Archive an item in a 1Password vault (instead of hard-deleting).
3+
*/
4+
5+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6+
import { z } from "zod";
7+
import { getClient } from "../client.js";
8+
import { log, logError } from "../logger.js";
9+
import { jsonResult, errorResult } from "../utils.js";
10+
11+
export function registerItemArchive(server: McpServer): void {
12+
server.tool(
13+
"item_archive",
14+
"Archive an item in a 1Password vault. The item is moved to the archive and hidden from regular views, rather than being permanently deleted.",
15+
{
16+
vaultId: z.string().min(1).describe("Vault ID containing the item."),
17+
itemId: z.string().min(1).describe("Item ID to archive."),
18+
},
19+
async ({ vaultId, itemId }) => {
20+
try {
21+
log("debug", "Tool call: item_archive.", { vaultId, itemId });
22+
const client = await getClient();
23+
if (!client?.items?.archive) {
24+
throw new Error(
25+
"Your @1password/sdk version does not support archiving items.",
26+
);
27+
}
28+
await client.items.archive(vaultId, itemId);
29+
return jsonResult({ archived: true, vaultId, itemId });
30+
} catch (error) {
31+
logError("item_archive failed.", error);
32+
return errorResult(error);
33+
}
34+
},
35+
);
36+
}

src/tools/item-edit.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* item_edit — Edit an existing 1Password item: title, notes, tags, url, and fields.
3+
*
4+
* The item is fetched, changes are applied immutably, and the result is written
5+
* back via items.put. Unreferenced fields are preserved untouched. Field values
6+
* are never logged.
7+
*/
8+
9+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10+
import { z } from "zod";
11+
import {
12+
ItemFieldType,
13+
AutofillBehavior,
14+
type Item,
15+
type ItemField,
16+
type Website,
17+
} from "@1password/sdk";
18+
import { getClient } from "../client.js";
19+
import { log, logError } from "../logger.js";
20+
import { jsonResult, errorResult } from "../utils.js";
21+
22+
/** A field upsert request from the caller. */
23+
const fieldInput = z.object({
24+
idOrTitle: z
25+
.string()
26+
.min(1)
27+
.describe("Field id or title to match (case-insensitive). Created if absent."),
28+
type: z
29+
.enum(["text", "concealed"])
30+
.describe("Field type: 'text' for plain values, 'concealed' for secrets."),
31+
value: z.string().describe("New value for the field."),
32+
section: z
33+
.string()
34+
.optional()
35+
.describe("Optional section id to associate the field with."),
36+
});
37+
38+
/** Does a field match the caller-supplied id-or-title? */
39+
function fieldMatches(field: ItemField, idOrTitle: string): boolean {
40+
const target = idOrTitle.toLowerCase();
41+
return (
42+
field.id?.toLowerCase() === target ||
43+
field.title?.toLowerCase() === target
44+
);
45+
}
46+
47+
export function registerItemEdit(server: McpServer): void {
48+
server.tool(
49+
"item_edit",
50+
"Edit an existing 1Password item. Update the title, notes (pass an empty string to clear notes), tags, website URL, upsert fields, or remove fields. Only referenced fields are changed; all others are preserved.",
51+
{
52+
vaultId: z.string().min(1).describe("Vault ID containing the item."),
53+
itemId: z.string().min(1).describe("Item ID to edit."),
54+
title: z.string().min(1).optional().describe("New item title."),
55+
notes: z
56+
.string()
57+
.optional()
58+
.describe(
59+
"Full replacement of the item's notes. Pass an empty string to clear notes.",
60+
),
61+
tags: z
62+
.array(z.string().min(1))
63+
.optional()
64+
.describe("Replacement set of tags (replaces all existing tags)."),
65+
url: z
66+
.string()
67+
.url()
68+
.optional()
69+
.describe("Replacement primary website URL for the item."),
70+
fields: z
71+
.array(fieldInput)
72+
.optional()
73+
.describe("Fields to upsert (create or update) by id or title."),
74+
removeFields: z
75+
.array(z.string().min(1))
76+
.optional()
77+
.describe("Field ids or titles to remove from the item."),
78+
},
79+
async ({ vaultId, itemId, title, notes, tags, url, fields, removeFields }) => {
80+
try {
81+
// NOTE: field values are intentionally excluded from logs.
82+
log("debug", "Tool call: item_edit.", {
83+
vaultId,
84+
itemId,
85+
changeTitle: title !== undefined,
86+
changeNotes: notes !== undefined,
87+
changeTags: tags !== undefined,
88+
changeUrl: url !== undefined,
89+
upsertCount: fields?.length ?? 0,
90+
removeCount: removeFields?.length ?? 0,
91+
});
92+
93+
const client = await getClient();
94+
if (!client?.items?.get) {
95+
throw new Error(
96+
"Your @1password/sdk version does not support getting items.",
97+
);
98+
}
99+
if (!client?.items?.put) {
100+
throw new Error(
101+
"Your @1password/sdk version does not support updating items.",
102+
);
103+
}
104+
105+
const existing: Item = await client.items.get(vaultId, itemId);
106+
107+
// Build the next field list immutably, preserving unreferenced fields.
108+
const removeSet = new Set(
109+
(removeFields ?? []).map((value) => value.toLowerCase()),
110+
);
111+
112+
let nextFields: ItemField[] = (existing.fields ?? []).filter(
113+
(field) =>
114+
!removeSet.has(field.id?.toLowerCase()) &&
115+
!removeSet.has(field.title?.toLowerCase()),
116+
);
117+
118+
for (const upsert of fields ?? []) {
119+
const fieldType =
120+
upsert.type === "concealed"
121+
? ItemFieldType.Concealed
122+
: ItemFieldType.Text;
123+
const index = nextFields.findIndex((field) =>
124+
fieldMatches(field, upsert.idOrTitle),
125+
);
126+
127+
if (index >= 0) {
128+
const current = nextFields[index];
129+
const replacement: ItemField = {
130+
...current,
131+
fieldType,
132+
value: upsert.value,
133+
sectionId: upsert.section ?? current.sectionId,
134+
};
135+
nextFields = nextFields.map((field, i) =>
136+
i === index ? replacement : field,
137+
);
138+
} else {
139+
nextFields = [
140+
...nextFields,
141+
{
142+
id: upsert.idOrTitle,
143+
title: upsert.idOrTitle,
144+
fieldType,
145+
value: upsert.value,
146+
sectionId: upsert.section,
147+
},
148+
];
149+
}
150+
}
151+
152+
let nextWebsites: Website[] | undefined = existing.websites;
153+
if (url !== undefined) {
154+
nextWebsites = [
155+
{
156+
url,
157+
label: "website",
158+
autofillBehavior: AutofillBehavior.AnywhereOnWebsite,
159+
},
160+
];
161+
}
162+
163+
const updated: Item = {
164+
...existing,
165+
title: title ?? existing.title,
166+
notes: notes ?? existing.notes,
167+
tags: tags ?? existing.tags,
168+
websites: nextWebsites,
169+
fields: nextFields,
170+
};
171+
172+
const result: Item = await client.items.put(updated);
173+
174+
return jsonResult({
175+
id: result.id,
176+
title: result.title,
177+
vaultId: result.vaultId,
178+
category: result.category,
179+
tags: result.tags ?? [],
180+
fieldCount: result.fields?.length ?? 0,
181+
updatedAt: result.updatedAt,
182+
});
183+
} catch (error) {
184+
logError("item_edit failed.", error);
185+
return errorResult(error);
186+
}
187+
},
188+
);
189+
}

0 commit comments

Comments
 (0)