From 7d3a49d60ae0a7c7f9d9c53332696c5ca98e5f60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 03:41:34 +0000 Subject: [PATCH 1/3] Initial plan From fd5f5339cb634abb92434cca15bb5691da924349 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 03:44:14 +0000 Subject: [PATCH 2/3] feat: support macos keychain token lookup Agent-Logs-Url: https://github.com/CakeRepository/1Password-MCP/sessions/4c5b132a-69be-40e9-ad6d-742b25bca276 Co-authored-by: CakeRepository <27045642+CakeRepository@users.noreply.github.com> --- README.md | 23 ++++++++++++ server.json | 18 ++++++++-- src/client.ts | 2 +- src/config.ts | 77 ++++++++++++++++++++++++++++++++++----- tests/config.test.ts | 85 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 191 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6e4b76e..b09b81f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,27 @@ A community-built [Model Context Protocol (MCP)](https://modelcontextprotocol.io } ``` +### macOS Keychain (JSON) + +If you do not want to store the service account token directly in your MCP config, macOS users can store it in Keychain and configure the server to read it at startup instead: + +```json +{ + "mcpServers": { + "1password": { + "command": "npx", + "args": ["-y", "@takescake/1password-mcp"], + "env": { + "OP_KEYCHAIN_SERVICE": "op-service-account-claude-automation", + "OP_KEYCHAIN_ACCOUNT": "your-macos-username" + } + } + } +} +``` + +`OP_SERVICE_ACCOUNT_TOKEN` still takes precedence when set. `OP_KEYCHAIN_ACCOUNT` is optional if your Keychain service name is already unique enough. + ### OpenAI Codex (TOML) **Option A** (stores the token in config): @@ -94,6 +115,8 @@ Then set `OP_SERVICE_ACCOUNT_TOKEN` in your shell/session/CI environment. > **Note:** `codex mcp add ... --env OP_SERVICE_ACCOUNT_TOKEN=...` writes the token into Codex config. Use `env_vars` if you want the config to reference only the variable name. +On macOS, you can also omit `OP_SERVICE_ACCOUNT_TOKEN` and set `OP_KEYCHAIN_SERVICE` (plus optional `OP_KEYCHAIN_ACCOUNT`) to read the token from Keychain at startup. + ### CLI Options ``` diff --git a/server.json b/server.json index b2688bd..763486d 100644 --- a/server.json +++ b/server.json @@ -18,10 +18,24 @@ "environmentVariables": [ { "name": "OP_SERVICE_ACCOUNT_TOKEN", - "description": "The Service Account Token from 1Password", - "isRequired": true, + "description": "The Service Account Token from 1Password (required unless OP_KEYCHAIN_SERVICE is used on macOS)", + "isRequired": false, "isSecret": true, "format": "string" + }, + { + "name": "OP_KEYCHAIN_SERVICE", + "description": "macOS only: Keychain service name to read the 1Password service account token from when OP_SERVICE_ACCOUNT_TOKEN is not set", + "isRequired": false, + "isSecret": false, + "format": "string" + }, + { + "name": "OP_KEYCHAIN_ACCOUNT", + "description": "macOS only: Optional Keychain account name to narrow the lookup used with OP_KEYCHAIN_SERVICE", + "isRequired": false, + "isSecret": false, + "format": "string" } ] } diff --git a/src/client.ts b/src/client.ts index ce56f1d..2cf6987 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,7 +16,7 @@ export function requireServiceAccountToken(): string { if (!config.serviceAccountToken) { log("error", "Missing service account token."); throw new Error( - "Service account token is required. Provide it via --service-account-token or OP_SERVICE_ACCOUNT_TOKEN.", + "Service account token is required. Provide it via --service-account-token, OP_SERVICE_ACCOUNT_TOKEN, or macOS Keychain with OP_KEYCHAIN_SERVICE.", ); } return config.serviceAccountToken; diff --git a/src/config.ts b/src/config.ts index d95383d..66174bf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,7 @@ * Server configuration: CLI arguments, environment variables, constants. */ +import { execFileSync } from "node:child_process"; import { LOG_LEVEL_VALUES, type LogLevel } from "./types.js"; export const SERVER_NAME = "1password-mcp"; @@ -31,11 +32,75 @@ export interface ServerConfig { /** Service account token (may be undefined until first use). */ serviceAccountToken: string | undefined; /** Where the token came from. */ - tokenSource: "args" | "env" | "missing"; + tokenSource: "args" | "env" | "keychain" | "missing"; } let _config: ServerConfig | undefined; +interface MacOsKeychainLookupOptions { + service?: string; + account?: string; + platform?: NodeJS.Platform; + execFileSyncImpl?: typeof execFileSync; +} + +export function readMacOsKeychainToken({ + service, + account, + platform = process.platform, + execFileSyncImpl = execFileSync, +}: MacOsKeychainLookupOptions): string | undefined { + if (!service || platform !== "darwin") return undefined; + + const args = ["find-generic-password"]; + if (account) args.push("-a", account); + args.push("-s", service, "-w"); + + try { + const token = execFileSyncImpl("security", args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + return token || undefined; + } catch { + return undefined; + } +} + +export function resolveServiceAccountToken({ + tokenFromArgs, + env = process.env, + readKeychainToken = readMacOsKeychainToken, +}: { + tokenFromArgs?: string; + env?: NodeJS.ProcessEnv; + readKeychainToken?: (options: { + service?: string; + account?: string; + }) => string | undefined; +} = {}): Pick { + const tokenFromEnv = env.OP_SERVICE_ACCOUNT_TOKEN; + const tokenFromKeychain = + tokenFromArgs || tokenFromEnv + ? undefined + : readKeychainToken({ + service: env.OP_KEYCHAIN_SERVICE, + account: env.OP_KEYCHAIN_ACCOUNT, + }); + + const serviceAccountToken = tokenFromArgs ?? tokenFromEnv ?? tokenFromKeychain; + + const tokenSource: ServerConfig["tokenSource"] = tokenFromArgs + ? "args" + : tokenFromEnv + ? "env" + : tokenFromKeychain + ? "keychain" + : "missing"; + + return { serviceAccountToken, tokenSource }; +} + /** Build and cache the server configuration. */ export function getConfig(): ServerConfig { if (_config) return _config; @@ -61,14 +126,8 @@ export function getConfig(): ServerConfig { const tokenFromArgs = getArgValue("service-account-token") ?? getArgValue("token"); - const serviceAccountToken = - tokenFromArgs ?? process.env.OP_SERVICE_ACCOUNT_TOKEN; - - const tokenSource: ServerConfig["tokenSource"] = tokenFromArgs - ? "args" - : process.env.OP_SERVICE_ACCOUNT_TOKEN - ? "env" - : "missing"; + const { serviceAccountToken, tokenSource } = + resolveServiceAccountToken({ tokenFromArgs }); _config = { logLevel: logLevelRaw, diff --git a/tests/config.test.ts b/tests/config.test.ts index be9c48d..002fd8f 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -3,8 +3,15 @@ */ import { readFileSync } from "node:fs"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { getConfig, resetConfig, SERVER_NAME, SERVER_VERSION } from "../src/config.js"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + getConfig, + readMacOsKeychainToken, + resetConfig, + resolveServiceAccountToken, + SERVER_NAME, + SERVER_VERSION, +} from "../src/config.js"; const packageJson = JSON.parse( readFileSync(new URL("../package.json", import.meta.url), "utf8"), @@ -22,6 +29,8 @@ describe("config", () => { delete process.env.OP_INTEGRATION_NAME; delete process.env.OP_INTEGRATION_VERSION; delete process.env.OP_SERVICE_ACCOUNT_TOKEN; + delete process.env.OP_KEYCHAIN_SERVICE; + delete process.env.OP_KEYCHAIN_ACCOUNT; }); afterEach(() => { @@ -103,6 +112,78 @@ describe("config", () => { expect(config.serviceAccountToken).toBe("arg-token"); }); + it("runs the expected macOS keychain lookup command", () => { + const execFileSyncImpl = vi.fn(() => "keychain-token\n"); + + const token = readMacOsKeychainToken({ + service: "op-service-account", + account: "alice", + platform: "darwin", + execFileSyncImpl, + }); + + expect(token).toBe("keychain-token"); + expect(execFileSyncImpl).toHaveBeenCalledWith("security", [ + "find-generic-password", + "-a", + "alice", + "-s", + "op-service-account", + "-w", + ], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + }); + + it("skips macOS keychain lookup on non-macOS platforms", () => { + const execFileSyncImpl = vi.fn(); + + const token = readMacOsKeychainToken({ + service: "op-service-account", + platform: "linux", + execFileSyncImpl, + }); + + expect(token).toBeUndefined(); + expect(execFileSyncImpl).not.toHaveBeenCalled(); + }); + + it("resolves token from macOS keychain when configured", () => { + const readKeychainToken = vi.fn(() => "keychain-token"); + + const config = resolveServiceAccountToken({ + env: { + OP_KEYCHAIN_SERVICE: "op-service-account", + OP_KEYCHAIN_ACCOUNT: "alice", + }, + readKeychainToken, + }); + + expect(config.tokenSource).toBe("keychain"); + expect(config.serviceAccountToken).toBe("keychain-token"); + expect(readKeychainToken).toHaveBeenCalledWith({ + service: "op-service-account", + account: "alice", + }); + }); + + it("prefers env token over macOS keychain lookup", () => { + const readKeychainToken = vi.fn(() => "keychain-token"); + + const config = resolveServiceAccountToken({ + env: { + OP_SERVICE_ACCOUNT_TOKEN: "env-token", + OP_KEYCHAIN_SERVICE: "op-service-account", + }, + readKeychainToken, + }); + + expect(config.tokenSource).toBe("env"); + expect(config.serviceAccountToken).toBe("env-token"); + expect(readKeychainToken).not.toHaveBeenCalled(); + }); + it("uses default integration name/version", () => { process.argv = ["node", "index.js"]; const config = getConfig(); From a6c051af92767b108f3572613d90389c3fc785bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 03:46:03 +0000 Subject: [PATCH 3/3] docs: clarify keychain token precedence Agent-Logs-Url: https://github.com/CakeRepository/1Password-MCP/sessions/4c5b132a-69be-40e9-ad6d-742b25bca276 Co-authored-by: CakeRepository <27045642+CakeRepository@users.noreply.github.com> --- README.md | 2 +- src/config.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b09b81f..e0a1c7f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ If you do not want to store the service account token directly in your MCP confi } ``` -`OP_SERVICE_ACCOUNT_TOKEN` still takes precedence when set. `OP_KEYCHAIN_ACCOUNT` is optional if your Keychain service name is already unique enough. +Precedence is: CLI arguments (`--service-account-token` / `--token`) > `OP_SERVICE_ACCOUNT_TOKEN` > macOS Keychain lookup. `OP_KEYCHAIN_ACCOUNT` is optional if your Keychain service name is already unique enough. ### OpenAI Codex (TOML) diff --git a/src/config.ts b/src/config.ts index 66174bf..57c2b45 100644 --- a/src/config.ts +++ b/src/config.ts @@ -80,13 +80,13 @@ export function resolveServiceAccountToken({ }) => string | undefined; } = {}): Pick { const tokenFromEnv = env.OP_SERVICE_ACCOUNT_TOKEN; - const tokenFromKeychain = - tokenFromArgs || tokenFromEnv - ? undefined - : readKeychainToken({ - service: env.OP_KEYCHAIN_SERVICE, - account: env.OP_KEYCHAIN_ACCOUNT, - }); + let tokenFromKeychain: string | undefined; + if (!tokenFromArgs && !tokenFromEnv) { + tokenFromKeychain = readKeychainToken({ + service: env.OP_KEYCHAIN_SERVICE, + account: env.OP_KEYCHAIN_ACCOUNT, + }); + } const serviceAccountToken = tokenFromArgs ?? tokenFromEnv ?? tokenFromKeychain;