@attrkit/mcp 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## 1.1.0 (unreleased: not yet published to npm)
3
+ ## 1.2.1 (2026-10-03)
4
+
5
+ ### Fixed
6
+
7
+ - `attrkit-mcp login` printed its prompt before it turned echo off, so a key that arrived in
8
+ between (a paste fired the moment the prompt appeared) was echoed on screen. Echo is now off
9
+ before the prompt is printed.
10
+
11
+ ## 1.2.0 (2026-10-03)
12
+
13
+ ### Added
14
+
15
+ - `attrkit-mcp login` (macOS): asks for the management key with echo off, checks it against the
16
+ API, and stores it in the Keychain item `attrkit-mcp` through `security -i` with the key on
17
+ stdin, so it never appears in argv. `attrkit-mcp logout` removes the item.
18
+ - The server reads the key from that Keychain item on macOS when `ATTRKIT_API_KEY` is unset, empty,
19
+ or an unexpanded client placeholder (`$ATTRKIT_API_KEY`, `${ATTRKIT_API_KEY}`,
20
+ `${ATTRKIT_API_KEY:-}`, `${env:ATTRKIT_API_KEY}`, `${input:attrkit-key}`). A real
21
+ `ATTRKIT_API_KEY` still wins and the Keychain is not consulted, so existing configurations behave
22
+ exactly as before.
23
+ - The missing-key error now mentions `npx -y @attrkit/mcp login` on macOS.
24
+
25
+ ## 1.1.0 (2026-10-01)
4
26
 
5
27
  ### Added
6
28
 
package/README.md CHANGED
@@ -45,15 +45,29 @@ Use a read-only or app-scoped key when the client needs less access. App-scoped
45
45
  keys cannot read or change the workspace usage cap, create sibling apps, or
46
46
  manage workspace-wide integrations.
47
47
 
48
- Load the key into the shell that starts your MCP client:
48
+ ### Give the server the key
49
+
50
+ On macOS, store the key once in the Keychain:
49
51
 
50
52
  ```bash
51
- printf "AttriKit management key: "
52
- IFS= read -r -s ATTRKIT_API_KEY
53
- export ATTRKIT_API_KEY
54
- printf "\n"
53
+ npx -y @attrkit/mcp login
55
54
  ```
56
55
 
56
+ It asks for the key with echo off, checks it against AttriKit (and prints the plan and scopes),
57
+ then stores it in the Keychain item `attrkit-mcp`. The key never appears in a process listing.
58
+ `npx -y @attrkit/mcp logout` removes the item.
59
+
60
+ On any system you can instead set `ATTRKIT_API_KEY` in the environment of the process that starts
61
+ your MCP client, or in the client's own secret store.
62
+
63
+ At start the server resolves the key in this order:
64
+
65
+ 1. `ATTRKIT_API_KEY`, when it holds a real value.
66
+ 2. On macOS, the Keychain item `attrkit-mcp`. An empty `ATTRKIT_API_KEY`, or one a client left
67
+ unexpanded (`$ATTRKIT_API_KEY`, `${ATTRKIT_API_KEY}`, `${ATTRKIT_API_KEY:-}`,
68
+ `${env:ATTRKIT_API_KEY}`, `${input:attrkit-key}`), counts as unset.
69
+ 3. Otherwise it exits with an error that names the fix.
70
+
57
71
  ## Codex
58
72
 
59
73
  Add this server to `~/.codex/config.toml`:
package/dist/cli.js CHANGED
@@ -14791,7 +14791,7 @@ function validatedApiBaseUrl(value) {
14791
14791
  function readConfig(env = process.env) {
14792
14792
  const apiKey = env.ATTRKIT_API_KEY;
14793
14793
  if (!apiKey) {
14794
- throw new Error("ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys).");
14794
+ throw new Error("ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys). On macOS, run `npx -y @attrkit/mcp login` to store it in the Keychain.");
14795
14795
  }
14796
14796
  const apiUrl = (env.ATTRKIT_API_URL ?? "https://attrikit.io").replace(/\/+$/, "");
14797
14797
  validatedApiBaseUrl(apiUrl);
@@ -15055,7 +15055,7 @@ ${body}`
15055
15055
  structuredContent: structured
15056
15056
  }, result.nextCursor);
15057
15057
  }
15058
- var SERVER_VERSION = "1.1.0";
15058
+ var SERVER_VERSION = "1.2.1";
15059
15059
  var periodSchema = z5.enum(["7d", "30d", "90d", "all"]).describe("Reporting window. Omit to use the endpoint's documented default.").optional();
15060
15060
  var appIdSchema = z5.string().describe("Optional app id to scope the result to one app.").optional();
15061
15061
  var requiredAppIdSchema = z5.string().min(1).describe("The AttriKit app id.");
@@ -15855,23 +15855,265 @@ function createServer(config, fetchImpl) {
15855
15855
  return server;
15856
15856
  }
15857
15857
 
15858
- // src/cli.ts
15859
- async function main() {
15858
+ // src/key.ts
15859
+ import { execFile } from "node:child_process";
15860
+ var KEYCHAIN_SERVICE = "attrkit-mcp";
15861
+ var SECURITY_BIN = "/usr/bin/security";
15862
+ var KEY_REQUIRED_MESSAGE = "ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys). On macOS, run `npx -y @attrkit/mcp login` to store it in the Keychain.";
15863
+ var KEY_REQUIRED_MESSAGE_OTHER_PLATFORM = "ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys). Set it in the environment or in your MCP client's secret store.";
15864
+ var UNEXPANDED_PLACEHOLDERS = new Set([
15865
+ "$ATTRKIT_API_KEY",
15866
+ "${ATTRKIT_API_KEY}",
15867
+ "${ATTRKIT_API_KEY:-}",
15868
+ "${env:ATTRKIT_API_KEY}",
15869
+ "${input:attrkit-key}"
15870
+ ]);
15871
+ function realEnvKey(env) {
15872
+ const value = env.ATTRKIT_API_KEY;
15873
+ if (value === undefined || value.trim() === "")
15874
+ return;
15875
+ if (UNEXPANDED_PLACEHOLDERS.has(value.trim()))
15876
+ return;
15877
+ return value;
15878
+ }
15879
+ var realExecFile = (file, args, options, done) => execFile(file, args, options, done);
15880
+ function createSecurityRunner(execFileImpl = realExecFile) {
15881
+ return (args, stdin) => new Promise((resolve) => {
15882
+ let writeFailed = false;
15883
+ const child = execFileImpl(SECURITY_BIN, [...args], { encoding: "utf8", timeout: 15000 }, (error, stdout, stderr) => {
15884
+ const code = error === null ? 0 : Number.isInteger(error.code) ? Number(error.code) : 1;
15885
+ resolve(writeFailed ? { code: code === 0 ? 1 : code, stdout, stderr: `${stderr}could not write to security` } : { code, stdout, stderr });
15886
+ });
15887
+ child.stdin?.on("error", () => {
15888
+ writeFailed = true;
15889
+ });
15890
+ if (stdin !== undefined)
15891
+ child.stdin?.end(stdin);
15892
+ else
15893
+ child.stdin?.end();
15894
+ });
15895
+ }
15896
+ var defaultSecurityRunner = createSecurityRunner();
15897
+ async function keychainKey(runSecurity) {
15898
+ const result = await runSecurity(["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"]);
15899
+ if (result.code !== 0)
15900
+ return;
15901
+ const key = result.stdout.replace(/\r?\n$/, "");
15902
+ return key.trim() === "" ? undefined : key;
15903
+ }
15904
+ async function resolveApiKey(options = {}) {
15905
+ const env = options.env ?? process.env;
15906
+ const platform = options.platform ?? process.platform;
15907
+ const fromEnv = realEnvKey(env);
15908
+ if (fromEnv !== undefined)
15909
+ return fromEnv;
15910
+ if (platform !== "darwin")
15911
+ throw new Error(KEY_REQUIRED_MESSAGE_OTHER_PLATFORM);
15912
+ const fromKeychain = await keychainKey(options.runSecurity ?? defaultSecurityRunner);
15913
+ if (fromKeychain !== undefined)
15914
+ return fromKeychain;
15915
+ throw new Error(KEY_REQUIRED_MESSAGE);
15916
+ }
15917
+
15918
+ // src/login.ts
15919
+ import { userInfo } from "node:os";
15920
+ import { z as z6 } from "zod";
15921
+ function quotedForSecurity(key) {
15922
+ if (key === "" || Array.from(key).some(isUnquotable))
15923
+ return null;
15924
+ return `"${key}"`;
15925
+ }
15926
+ function isUnquotable(character) {
15927
+ const code = character.charCodeAt(0);
15928
+ return character === '"' || character === "\\" || code <= 31 || code === 127;
15929
+ }
15930
+ function addKeyCommand(username, quotedKey) {
15931
+ return `add-generic-password -U -s ${KEYCHAIN_SERVICE} -a "${Array.from(username).filter((character) => !isUnquotable(character)).join("")}" -w ${quotedKey}
15932
+ `;
15933
+ }
15934
+ function readHidden(input, prompt, write, idleMs = 50) {
15935
+ return new Promise((resolve) => {
15936
+ input.setRawMode(true);
15937
+ write(prompt);
15938
+ input.resume();
15939
+ input.setEncoding("utf8");
15940
+ let value = "";
15941
+ let mode = "text";
15942
+ let idle;
15943
+ const finish = (result) => {
15944
+ if (idle !== undefined)
15945
+ clearTimeout(idle);
15946
+ input.removeListener("data", onData);
15947
+ input.setRawMode(false);
15948
+ input.pause();
15949
+ write(`
15950
+ `);
15951
+ resolve(result);
15952
+ };
15953
+ const armIdle = () => {
15954
+ if (idle !== undefined)
15955
+ clearTimeout(idle);
15956
+ idle = setTimeout(() => finish(value), idleMs);
15957
+ };
15958
+ const onData = (chunk) => {
15959
+ for (const character of chunk) {
15960
+ if (mode === "drain")
15961
+ continue;
15962
+ if (mode === "escape") {
15963
+ mode = character === "[" ? "csi" : character === "O" ? "ss3" : "text";
15964
+ } else if (mode === "csi") {
15965
+ if (character >= "@" && character <= "~")
15966
+ mode = "text";
15967
+ } else if (mode === "ss3") {
15968
+ mode = "text";
15969
+ } else if (character === "\x03") {
15970
+ return finish(null);
15971
+ } else if (character === "\x1B") {
15972
+ mode = "escape";
15973
+ } else if (character === "\r" || character === `
15974
+ `) {
15975
+ mode = "drain";
15976
+ } else if (character === "" || character === "\b") {
15977
+ value = value.slice(0, -1);
15978
+ } else if (character >= " ") {
15979
+ value += character;
15980
+ }
15981
+ }
15982
+ if (mode === "drain")
15983
+ armIdle();
15984
+ };
15985
+ input.on("data", onData);
15986
+ });
15987
+ }
15988
+ function readHiddenFromTty(prompt) {
15989
+ return readHidden(process.stdin, prompt, (text) => void process.stderr.write(text));
15990
+ }
15991
+ var whoamiSchema = z6.object({
15992
+ data: z6.object({
15993
+ plan: z6.string().optional(),
15994
+ scope: z6.object({
15995
+ app_id: z6.string().nullish(),
15996
+ scopes: z6.array(z6.string()).optional()
15997
+ }).optional()
15998
+ })
15999
+ });
16000
+ function describeWhoami(json) {
16001
+ const parsed = whoamiSchema.safeParse(json);
16002
+ if (!parsed.success)
16003
+ return "Key accepted.";
16004
+ const { plan, scope } = parsed.data.data;
16005
+ const app = scope?.app_id ? `app ${scope.app_id}` : "all apps";
16006
+ return `Key accepted. Plan: ${plan ?? "unknown"}. Scopes: ${scope?.scopes?.join(", ") ?? "unknown"}. Restriction: ${app}.`;
16007
+ }
16008
+ async function runLogin(options = {}) {
16009
+ const write = options.write ?? ((text) => void process.stderr.write(text));
16010
+ const platform = options.platform ?? process.platform;
16011
+ if (platform !== "darwin") {
16012
+ write(`attrkit-mcp login stores the key in the macOS Keychain. On this system set ATTRKIT_API_KEY in the environment or in your MCP client's secret store.
16013
+ `);
16014
+ return 1;
16015
+ }
16016
+ const isTTY = options.isTTY ?? Boolean(process.stdin.isTTY);
16017
+ if (!isTTY) {
16018
+ write(`attrkit-mcp login needs an interactive terminal to ask for the key.
16019
+ `);
16020
+ return 1;
16021
+ }
16022
+ const readSecret = options.readSecret ?? readHiddenFromTty;
16023
+ const entered = await readSecret("AttriKit management key: ");
16024
+ if (entered === null)
16025
+ return 130;
16026
+ const key = entered.trim();
16027
+ const quoted = quotedForSecurity(key);
16028
+ if (quoted === null) {
16029
+ write(`That key was not accepted: it is empty or holds a quote, backslash or control character. Nothing was stored.
16030
+ `);
16031
+ return 1;
16032
+ }
15860
16033
  let config;
15861
16034
  try {
15862
- config = readConfig();
16035
+ config = readConfig({ ...options.env ?? process.env, ATTRKIT_API_KEY: key });
15863
16036
  } catch (error) {
15864
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}
15865
- `, () => process.exit(1));
16037
+ write(`${error instanceof Error ? error.message : String(error)}
16038
+ `);
16039
+ return 1;
16040
+ }
16041
+ const result = await callApi(config, "/api/v1/whoami", options.fetchImpl ? { fetchImpl: options.fetchImpl } : {});
16042
+ if (result.status === 401 || result.status === 403) {
16043
+ write(`AttriKit refused that key. Nothing was stored.
16044
+ `);
16045
+ return 1;
16046
+ }
16047
+ if (!result.ok) {
16048
+ write(result.status === 0 ? `Could not reach ${new URL(config.apiUrl).origin} to check the key. Nothing was stored.
16049
+ ` : `Could not check the key (HTTP ${result.status}). Nothing was stored.
16050
+ `);
16051
+ return 1;
16052
+ }
16053
+ write(`${describeWhoami(result.json)}
16054
+ `);
16055
+ const runSecurity = options.runSecurity ?? defaultSecurityRunner;
16056
+ const stored = await runSecurity(["-i"], addKeyCommand(options.username ?? userInfo().username, quoted));
16057
+ if (stored.code !== 0 || stored.stderr.trim() !== "") {
16058
+ write(`The Keychain refused the item (exit ${stored.code}). Nothing was stored.
16059
+ `);
16060
+ return 1;
16061
+ }
16062
+ write(`Stored in the macOS Keychain as attrkit-mcp. Your MCP client can now start the server without ATTRKIT_API_KEY.
16063
+ `);
16064
+ return 0;
16065
+ }
16066
+ async function runLogout(options = {}) {
16067
+ const write = options.write ?? ((text) => void process.stderr.write(text));
16068
+ if ((options.platform ?? process.platform) !== "darwin") {
16069
+ write(`attrkit-mcp logout removes the macOS Keychain item; there is nothing to remove on this system.
16070
+ `);
16071
+ return 1;
16072
+ }
16073
+ const result = await (options.runSecurity ?? defaultSecurityRunner)(["delete-generic-password", "-s", KEYCHAIN_SERVICE]);
16074
+ write(result.code === 0 ? `Removed the attrkit-mcp item from the macOS Keychain.
16075
+ ` : `Nothing was stored in the Keychain for attrkit-mcp.
16076
+ `);
16077
+ return 0;
16078
+ }
16079
+
16080
+ // src/main.ts
16081
+ function defaultDeps(startServer) {
16082
+ return {
16083
+ runLogin,
16084
+ runLogout,
16085
+ resolveApiKey,
16086
+ startServer,
16087
+ fail: (message, code) => void process.stderr.write(`${message}
16088
+ `, () => process.exit(code)),
16089
+ exit: (code) => process.exit(code)
16090
+ };
16091
+ }
16092
+ async function runCli(argv, deps) {
16093
+ const command = argv[2];
16094
+ if (command === "login")
16095
+ return deps.exit(await deps.runLogin());
16096
+ if (command === "logout")
16097
+ return deps.exit(await deps.runLogout());
16098
+ let config;
16099
+ try {
16100
+ const apiKey = await deps.resolveApiKey();
16101
+ config = readConfig({ ...process.env, ATTRKIT_API_KEY: apiKey });
16102
+ } catch (error) {
16103
+ deps.fail(error instanceof Error ? error.message : String(error), 1);
15866
16104
  return;
15867
16105
  }
16106
+ await deps.startServer(config);
16107
+ }
16108
+
16109
+ // src/cli.ts
16110
+ runCli(process.argv, defaultDeps(async (config) => {
15868
16111
  const server = createServer(config);
15869
16112
  const transport = new StdioServerTransport;
15870
16113
  await server.connect(transport);
15871
16114
  process.stderr.write(`attrkit-mcp connected (api=${config.apiUrl})
15872
16115
  `);
15873
- }
15874
- main().catch((error) => {
16116
+ })).catch((error) => {
15875
16117
  process.stderr.write(`attrkit-mcp failed to start: ${error instanceof Error ? error.stack ?? error.message : String(error)}
15876
16118
  `, () => process.exit(1));
15877
16119
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attrkit/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "AttriKit's stdio MCP server for its public Management API",
5
5
  "keywords": [
6
6
  "attrkit",
@@ -34,7 +34,7 @@
34
34
  "prepack": "bun run build",
35
35
  "typecheck": "../../node_modules/typescript-7/bin/tsc --noEmit",
36
36
  "lint": "eslint .",
37
- "test": "cd ../.. && vitest run packages/mcp/src/index.test.ts"
37
+ "test": "cd ../.. && vitest run packages/mcp/src"
38
38
  },
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "1.30.0",