Skip to content
Merged
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
```

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)

**Option A** (stores the token in config):
Expand All @@ -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

```
Expand Down
18 changes: 16 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 68 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<ServerConfig, "serviceAccountToken" | "tokenSource"> {
const tokenFromEnv = env.OP_SERVICE_ACCOUNT_TOKEN;
let tokenFromKeychain: string | undefined;
if (!tokenFromArgs && !tokenFromEnv) {
tokenFromKeychain = 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;
Expand All @@ -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,
Expand Down
85 changes: 83 additions & 2 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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();
Expand Down
Loading