diff --git a/src/tools/index.ts b/src/tools/index.ts index c7a66cc..6058a34 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -11,6 +11,7 @@ import { registerPasswordRead } from "./password-read.js"; import { registerPasswordUpdate } from "./password-update.js"; import { registerPasswordGenerate } from "./password-generate.js"; import { registerPasswordGenerateMemorable } from "./password-generate-memorable.js"; +import { registerNoteCreate } from "./note-create.js"; /** Register every tool with the MCP server. */ export function registerAllTools(server: McpServer): void { @@ -22,4 +23,5 @@ export function registerAllTools(server: McpServer): void { registerPasswordUpdate(server); registerPasswordGenerate(server); registerPasswordGenerateMemorable(server); + registerNoteCreate(server); } diff --git a/src/tools/note-create.ts b/src/tools/note-create.ts new file mode 100644 index 0000000..720429a --- /dev/null +++ b/src/tools/note-create.ts @@ -0,0 +1,78 @@ +/** + * note_create — Create a new Secure Note item in a 1Password vault. + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import * as sdk from "@1password/sdk"; +import { getClient } from "../client.js"; +import { log, logError } from "../logger.js"; +import { jsonResult, errorResult } from "../utils.js"; + +const fieldSchema = z.object({ + idOrTitle: z.string().min(1).describe("Field id or title."), + type: z.enum(["text", "concealed"]).describe("Field type."), + value: z.string().describe("Field value."), + section: z.string().min(1).optional().describe("Optional section id to place the field in."), +}); + +export function registerNoteCreate(server: McpServer): void { + server.tool( + "note_create", + "Create a new Secure Note item in a 1Password vault with optional tags and custom fields.", + { + vaultId: z.string().min(1).describe("Vault ID to create the note in."), + title: z.string().min(1).describe("Note title."), + notes: z.string().optional().describe("Note body text."), + tags: z + .array(z.string().min(1)) + .optional() + .describe("Optional tags for organization."), + fields: z + .array(fieldSchema) + .optional() + .describe("Optional custom fields to add to the note."), + }, + async ({ vaultId, title, notes, tags, fields }) => { + try { + log("debug", "Tool call: note_create.", { vaultId, title }); + const client = await getClient(); + if (!client?.items?.create) { + throw new Error( + "Your @1password/sdk version does not support creating items.", + ); + } + + const itemFields: sdk.ItemField[] = (fields ?? []).map((f) => ({ + id: f.idOrTitle, + title: f.idOrTitle, + fieldType: + f.type === "concealed" + ? sdk.ItemFieldType.Concealed + : sdk.ItemFieldType.Text, + value: f.value, + ...(f.section !== undefined ? { sectionId: f.section } : {}), + })); + + const item = await client.items.create({ + title, + category: sdk.ItemCategory.SecureNote, + vaultId, + fields: itemFields, + tags, + notes, + }); + + return jsonResult({ + id: item.id, + title: item.title, + vaultId: (item as { vaultId?: string }).vaultId ?? vaultId, + category: item.category, + }); + } catch (error) { + logError("note_create failed.", error); + return errorResult(error); + } + }, + ); +} diff --git a/tests/tools.test.ts b/tests/tools.test.ts index c515b3b..dc95da1 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -49,8 +49,8 @@ describe("MCP Tools", () => { registerAllTools(server); }); - it("registers all 8 tools", () => { - expect(registeredTools.size).toBe(8); + it("registers all 9 tools", () => { + expect(registeredTools.size).toBe(9); expect(registeredTools.has("vault_list")).toBe(true); expect(registeredTools.has("item_lookup")).toBe(true); expect(registeredTools.has("item_delete")).toBe(true); @@ -59,6 +59,7 @@ describe("MCP Tools", () => { expect(registeredTools.has("password_update")).toBe(true); expect(registeredTools.has("password_generate")).toBe(true); expect(registeredTools.has("password_generate_memorable")).toBe(true); + expect(registeredTools.has("note_create")).toBe(true); }); it("all tools have descriptions", () => { @@ -234,4 +235,85 @@ describe("MCP Tools", () => { expect(result.content[0].text).toContain("Provide secretReference or both vaultId and itemId"); }); }); + + describe("note_create", () => { + it("creates a secure note and returns metadata", async () => { + mockedGetClient.mockResolvedValue({ + items: { + create: vi.fn().mockResolvedValue({ + id: "n1", + title: "My Note", + vaultId: "v1", + category: "SecureNote", + }), + }, + } as any); + + const handler = registeredTools.get("note_create")!.handler; + const result = await handler({ + vaultId: "v1", + title: "My Note", + notes: "Some note text", + }); + const data = JSON.parse(result.content[0].text); + + expect(result.isError).toBeUndefined(); + expect(data.id).toBe("n1"); + expect(data.title).toBe("My Note"); + expect(data.category).toBe("SecureNote"); + }); + + it("creates a secure note with tags and custom fields", async () => { + const mockCreate = vi.fn().mockResolvedValue({ + id: "n2", + title: "Tagged Note", + vaultId: "v1", + category: "SecureNote", + }); + mockedGetClient.mockResolvedValue({ + items: { create: mockCreate }, + } as any); + + const handler = registeredTools.get("note_create")!.handler; + await handler({ + vaultId: "v1", + title: "Tagged Note", + notes: "body", + tags: ["work", "secret"], + fields: [ + { idOrTitle: "apiKey", type: "concealed", value: "abc123" }, + { idOrTitle: "env", type: "text", value: "production", section: "s1" }, + ], + }); + + expect(mockCreate).toHaveBeenCalledOnce(); + const callArg = mockCreate.mock.calls[0][0]; + expect(callArg.category).toBe("SecureNote"); + expect(callArg.tags).toEqual(["work", "secret"]); + expect(callArg.fields).toHaveLength(2); + expect(callArg.fields[0].fieldType).toBe("Concealed"); + expect(callArg.fields[1].fieldType).toBe("Text"); + expect(callArg.fields[1].sectionId).toBe("s1"); + }); + + it("returns error when SDK fails", async () => { + mockedGetClient.mockRejectedValue(new Error("create failed")); + + const handler = registeredTools.get("note_create")!.handler; + const result = await handler({ vaultId: "v1", title: "Note" }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("create failed"); + }); + + it("returns error when items.create is not available", async () => { + mockedGetClient.mockResolvedValue({ items: {} } as any); + + const handler = registeredTools.get("note_create")!.handler; + const result = await handler({ vaultId: "v1", title: "Note" }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("does not support creating items"); + }); + }); });