Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,4 +23,5 @@ export function registerAllTools(server: McpServer): void {
registerPasswordUpdate(server);
registerPasswordGenerate(server);
registerPasswordGenerateMemorable(server);
registerNoteCreate(server);
}
78 changes: 78 additions & 0 deletions src/tools/note-create.ts
Original file line number Diff line number Diff line change
@@ -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);
}
},
);
}
86 changes: 84 additions & 2 deletions tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
});