@attrkit/mcp 1.1.0 → 1.2.0
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 +15 -1
- package/README.md +19 -5
- package/dist/cli.js +251 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.2.0 (2026-10-03)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `attrkit-mcp login` (macOS): asks for the management key with echo off, checks it against the
|
|
8
|
+
API, and stores it in the Keychain item `attrkit-mcp` through `security -i` with the key on
|
|
9
|
+
stdin, so it never appears in argv. `attrkit-mcp logout` removes the item.
|
|
10
|
+
- The server reads the key from that Keychain item on macOS when `ATTRKIT_API_KEY` is unset, empty,
|
|
11
|
+
or an unexpanded client placeholder (`$ATTRKIT_API_KEY`, `${ATTRKIT_API_KEY}`,
|
|
12
|
+
`${ATTRKIT_API_KEY:-}`, `${env:ATTRKIT_API_KEY}`, `${input:attrkit-key}`). A real
|
|
13
|
+
`ATTRKIT_API_KEY` still wins and the Keychain is not consulted, so existing configurations behave
|
|
14
|
+
exactly as before.
|
|
15
|
+
- The missing-key error now mentions `npx -y @attrkit/mcp login` on macOS.
|
|
16
|
+
|
|
17
|
+
## 1.1.0 (2026-10-01)
|
|
4
18
|
|
|
5
19
|
### Added
|
|
6
20
|
|
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
|
-
|
|
48
|
+
### Give the server the key
|
|
49
|
+
|
|
50
|
+
On macOS, store the key once in the Keychain:
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
|
-
|
|
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.
|
|
15058
|
+
var SERVER_VERSION = "1.2.0";
|
|
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/
|
|
15859
|
-
|
|
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
|
+
write(prompt);
|
|
15937
|
+
input.setRawMode(true);
|
|
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
|
-
|
|
15865
|
-
|
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
|
37
|
+
"test": "cd ../.. && vitest run packages/mcp/src"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "1.30.0",
|