frinus-mcp 3.1.0 → 3.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/README.md +41 -0
- package/dist/tools/definitions.js +66 -6
- package/dist/tools/handlers.js +292 -4
- package/manifest.json +1 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -307,6 +307,47 @@ Reusable behaviours assigned to agents.
|
|
|
307
307
|
| `skill_remove` | Detach a skill from an agent. |
|
|
308
308
|
| `skill_agent_list` | List the skills owned by a given agent. |
|
|
309
309
|
|
|
310
|
+
### Credential Vault (5) — `credential_exec` broker
|
|
311
|
+
|
|
312
|
+
Encrypted credentials stored in the Control Plane, referenced from memories via
|
|
313
|
+
`credential_ref`.
|
|
314
|
+
|
|
315
|
+
| Tool | Description |
|
|
316
|
+
|------|-------------|
|
|
317
|
+
| `credential_store` | Store an encrypted credential under a ref (e.g. `mysql_x`). |
|
|
318
|
+
| `credential_get` | Inspect **non-secret metadata only** (host/user/db/port + the env vars `credential_exec` will inject). Never returns the value. |
|
|
319
|
+
| `credential_exec` | **Run a command with the credential injected into the child process ENV.** Returns only stdout/stderr/exit_code — the secret never reaches the model, screen, or disk. |
|
|
320
|
+
| `credential_list` | List stored credential refs (no secret data). |
|
|
321
|
+
| `credential_delete` | Delete a stored credential. |
|
|
322
|
+
|
|
323
|
+
**Secret-handling principle — server-side broker, never by value.** The model
|
|
324
|
+
never receives a secret value: no plaintext, no temp files, no shell snippets that
|
|
325
|
+
carry the secret. To *use* a credential you call **`credential_exec`**. The MCP
|
|
326
|
+
server (already running locally via `npx -y frinus-mcp@latest`) fetches the
|
|
327
|
+
credential from the vault, injects its fields into the **environment** of a child
|
|
328
|
+
process — never into argv, never into any text the model sees — runs the command
|
|
329
|
+
with `shell:false` (no shell injection), and returns only the output.
|
|
330
|
+
|
|
331
|
+
```jsonc
|
|
332
|
+
// MYSQL_PWD / MYSQL_USER / MYSQL_HOST are pre-injected → standard clients just work
|
|
333
|
+
credential_exec(ref="mysql_prod", argv=["mysql", "-e", "SELECT 1"])
|
|
334
|
+
credential_exec(ref="pg_prod", argv=["psql", "-c", "SELECT 1"])
|
|
335
|
+
|
|
336
|
+
// For anything else, read the injected vars inside an explicit shell:
|
|
337
|
+
credential_exec(ref="jira_x", argv=["sh","-c",
|
|
338
|
+
"curl -sS -H \"Authorization: Bearer $CRED_TOKEN\" \"$CRED_BASE_URL/whoami\""])
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Injected env vars (when present in the credential): password/secret/token →
|
|
342
|
+
`MYSQL_PWD`, `PGPASSWORD`, `CRED_PASSWORD`; `user`/`username` → `CRED_USER`,
|
|
343
|
+
`MYSQL_USER`, `PGUSER`; `host` → `CRED_HOST`, `MYSQL_HOST`, `PGHOST`; `port` →
|
|
344
|
+
`CRED_PORT`, `MYSQL_TCP_PORT`, `PGPORT`; `database` → `CRED_DATABASE`,
|
|
345
|
+
`PGDATABASE`; any other scalar → `CRED_<UPPER_SNAKE>` (e.g. `base_url` →
|
|
346
|
+
`CRED_BASE_URL`). `argv` must be an array of strings (no shell command string;
|
|
347
|
+
use `["sh","-c","..."]` if you really need a shell). 30s timeout, 256 KiB output
|
|
348
|
+
cap. Everything ships in the npm package — **the user installs nothing, edits no
|
|
349
|
+
PATH, and runs no extra command**; an up-to-date MCP is all that's required.
|
|
350
|
+
|
|
310
351
|
## Memory Types
|
|
311
352
|
|
|
312
353
|
| Type | Use Case | Example |
|
|
@@ -1499,24 +1499,84 @@ to link a memory to this credential.`,
|
|
|
1499
1499
|
},
|
|
1500
1500
|
{
|
|
1501
1501
|
name: "credential_get",
|
|
1502
|
-
description: `
|
|
1502
|
+
description: `Inspect a credential's NON-SECRET metadata — never its value.
|
|
1503
1503
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1504
|
+
This tool returns ONLY non-secret metadata (host/user/db/port), the names of the
|
|
1505
|
+
secret fields, and the names of the environment variables that 'credential_exec'
|
|
1506
|
+
will inject. The secret VALUE is NEVER returned, so it can never be echoed inline
|
|
1507
|
+
into a shell command and leaked on a screen share.
|
|
1506
1508
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
+
To actually USE the credential (connect to a DB, call an API), do NOT request the
|
|
1510
|
+
value — call 'credential_exec'. It fetches the secret on the server side, injects
|
|
1511
|
+
it into the child process environment, runs your command, and returns only the
|
|
1512
|
+
output. The secret never reaches you, the screen, or disk.
|
|
1513
|
+
|
|
1514
|
+
Only the credential owner (authenticated API key) can access their credentials.`,
|
|
1509
1515
|
inputSchema: {
|
|
1510
1516
|
type: "object",
|
|
1511
1517
|
properties: {
|
|
1512
1518
|
ref: {
|
|
1513
1519
|
type: "string",
|
|
1514
|
-
description: "Credential reference to
|
|
1520
|
+
description: "Credential reference to inspect (e.g., 'jira_muza')",
|
|
1515
1521
|
},
|
|
1516
1522
|
},
|
|
1517
1523
|
required: ["ref"],
|
|
1518
1524
|
},
|
|
1519
1525
|
},
|
|
1526
|
+
{
|
|
1527
|
+
name: "credential_exec",
|
|
1528
|
+
description: `Run a command with a vault credential injected into its environment — the secret NEVER reaches you.
|
|
1529
|
+
|
|
1530
|
+
This is the secure way to USE a stored credential. The MCP server (running locally
|
|
1531
|
+
via npx) fetches the credential from the vault, injects its fields into the
|
|
1532
|
+
ENVIRONMENT of a child process (never into argv, never into any text you see),
|
|
1533
|
+
runs your command with shell:false (no shell injection), and returns ONLY
|
|
1534
|
+
stdout, stderr, and exit_code. The secret value is never returned to you, never
|
|
1535
|
+
printed on screen, never logged, and never written to disk.
|
|
1536
|
+
|
|
1537
|
+
Injected environment variables (when present in the credential):
|
|
1538
|
+
- password/secret/token field -> MYSQL_PWD, PGPASSWORD, CRED_PASSWORD
|
|
1539
|
+
- user / username -> CRED_USER, MYSQL_USER, PGUSER
|
|
1540
|
+
- host / hostname -> CRED_HOST, MYSQL_HOST, PGHOST
|
|
1541
|
+
- port -> CRED_PORT, MYSQL_TCP_PORT, PGPORT
|
|
1542
|
+
- database / db / dbname -> CRED_DATABASE, PGDATABASE
|
|
1543
|
+
- any other scalar field -> CRED_<UPPER_SNAKE_NAME> (e.g. base_url -> CRED_BASE_URL)
|
|
1544
|
+
|
|
1545
|
+
Because MYSQL_PWD / PGPASSWORD etc. are standard, common clients pick them up
|
|
1546
|
+
with no flags:
|
|
1547
|
+
credential_exec(ref="mysql_prod", argv=["mysql", "-e", "SELECT 1"])
|
|
1548
|
+
credential_exec(ref="pg_prod", argv=["psql", "-c", "SELECT 1"])
|
|
1549
|
+
|
|
1550
|
+
For anything else, read the injected vars inside an explicit shell — the secret
|
|
1551
|
+
is expanded inside the child, never in text you author:
|
|
1552
|
+
credential_exec(ref="jira_x", argv=["sh", "-c",
|
|
1553
|
+
"curl -sS -H \\"Authorization: Bearer $CRED_TOKEN\\" \\"$CRED_BASE_URL/whoami\\""])
|
|
1554
|
+
|
|
1555
|
+
RULES:
|
|
1556
|
+
- 'argv' MUST be an array of strings (program first, then args). A shell command
|
|
1557
|
+
string is NOT accepted; pass ["sh","-c","..."] only when you genuinely need a shell.
|
|
1558
|
+
- NEVER put a secret value in argv or ask for the value — there is no path to it.
|
|
1559
|
+
- 30s timeout; output capped at 256 KiB per stream.`,
|
|
1560
|
+
inputSchema: {
|
|
1561
|
+
type: "object",
|
|
1562
|
+
properties: {
|
|
1563
|
+
ref: {
|
|
1564
|
+
type: "string",
|
|
1565
|
+
description: "Credential reference to use (e.g., 'mysql_centerpag', 'jira_muza').",
|
|
1566
|
+
},
|
|
1567
|
+
argv: {
|
|
1568
|
+
type: "array",
|
|
1569
|
+
items: { type: "string" },
|
|
1570
|
+
description: "Command as an array: program first, then each argument as a separate string. E.g. [\"mysql\", \"-e\", \"SELECT 1\"]. NOT a shell string.",
|
|
1571
|
+
},
|
|
1572
|
+
stdin: {
|
|
1573
|
+
type: "string",
|
|
1574
|
+
description: "Optional data to pipe to the command's stdin (e.g. a SQL script).",
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
required: ["ref", "argv"],
|
|
1578
|
+
},
|
|
1579
|
+
},
|
|
1520
1580
|
{
|
|
1521
1581
|
name: "credential_list",
|
|
1522
1582
|
description: `List all stored credentials for the current user.
|
package/dist/tools/handlers.js
CHANGED
|
@@ -5,8 +5,166 @@
|
|
|
5
5
|
* and return formatted MCP tool results.
|
|
6
6
|
*/
|
|
7
7
|
import axios from "axios";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
8
9
|
import { getResolvedTenantOrgId } from "../client/memory-client.js";
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
11
|
+
// Credential delivery — server-side broker, never by value
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// SECURITY: agents running in Claude Code render the full bash command string
|
|
14
|
+
// on screen. If a secret value ever enters the agent's text context, the agent
|
|
15
|
+
// can (and historically did) splice it inline into a command like
|
|
16
|
+
// `export MYSQL_PWD='...'`, leaking it in plaintext during screen sharing.
|
|
17
|
+
//
|
|
18
|
+
// The structural fix is to NEVER return the secret value to the model at all,
|
|
19
|
+
// and — going one step further — to never even require the agent to hold the
|
|
20
|
+
// secret as a shell-side reference. The MCP server already runs locally (via
|
|
21
|
+
// `npx -y frinus-mcp@latest`) and already fetches the credential from the
|
|
22
|
+
// Control Plane vault. So the broker lives INSIDE this process as a native
|
|
23
|
+
// tool: `credential_exec`.
|
|
24
|
+
//
|
|
25
|
+
// `credential_exec` fetches the credential, injects the fields into the ENV of
|
|
26
|
+
// a child process (never into argv, never into the model's text), spawns the
|
|
27
|
+
// command with shell:false, and returns ONLY stdout/stderr/exit_code. The
|
|
28
|
+
// secret never enters the tool result, is never logged, and never touches disk.
|
|
29
|
+
// Everything ships in the npm package — the user installs nothing, edits no
|
|
30
|
+
// PATH, and runs no extra command.
|
|
31
|
+
//
|
|
32
|
+
// `credential_get` no longer surfaces any secret-bearing snippet: it returns
|
|
33
|
+
// non-sensitive metadata only and points the agent at `credential_exec`.
|
|
34
|
+
/** Keys (case-insensitive) treated as sensitive when surfacing metadata. */
|
|
35
|
+
const SECRET_KEY_RE = /pass(word|wd)?|secret|token|api[_-]?key|private[_-]?key|credential/i;
|
|
36
|
+
function looksLikePassword(key) {
|
|
37
|
+
return SECRET_KEY_RE.test(key);
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// credential_exec internals
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const CRED_EXEC_TIMEOUT_MS = 30_000;
|
|
43
|
+
const CRED_EXEC_MAX_OUTPUT = 256 * 1024; // 256 KiB cap per stream
|
|
44
|
+
/**
|
|
45
|
+
* Map credential fields onto environment variables for the child process.
|
|
46
|
+
* The secret value flows ONLY into this env map — never into argv, never into
|
|
47
|
+
* any returned/ logged text.
|
|
48
|
+
*
|
|
49
|
+
* - Password-like field -> MYSQL_PWD, PGPASSWORD, CRED_PASSWORD
|
|
50
|
+
* - user/username -> CRED_USER (+ MYSQL_USER, PGUSER)
|
|
51
|
+
* - host/hostname -> CRED_HOST (+ MYSQL_HOST, PGHOST)
|
|
52
|
+
* - port -> CRED_PORT (+ MYSQL_TCP_PORT, PGPORT)
|
|
53
|
+
* - database/db/dbname -> CRED_DATABASE (+ PGDATABASE)
|
|
54
|
+
* - any other scalar -> CRED_<UPPER_SNAKE> (e.g. base_url -> CRED_BASE_URL)
|
|
55
|
+
*/
|
|
56
|
+
function buildCredentialEnv(data) {
|
|
57
|
+
const env = {};
|
|
58
|
+
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
59
|
+
let passwordSet = false;
|
|
60
|
+
let userSet = false;
|
|
61
|
+
let hostSet = false;
|
|
62
|
+
let portSet = false;
|
|
63
|
+
let dbSet = false;
|
|
64
|
+
for (const [k, v] of Object.entries(data)) {
|
|
65
|
+
if (!isScalar(v))
|
|
66
|
+
continue;
|
|
67
|
+
const val = String(v);
|
|
68
|
+
const lk = k.toLowerCase();
|
|
69
|
+
// Generic CRED_<NAME> for every scalar field (raw key, upper-snaked).
|
|
70
|
+
const credKey = "CRED_" + k.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
71
|
+
env[credKey] = val;
|
|
72
|
+
if (!passwordSet && looksLikePassword(k)) {
|
|
73
|
+
env.CRED_PASSWORD = val;
|
|
74
|
+
env.MYSQL_PWD = val;
|
|
75
|
+
env.PGPASSWORD = val;
|
|
76
|
+
passwordSet = true;
|
|
77
|
+
}
|
|
78
|
+
else if (!userSet && (lk === "user" || lk === "username")) {
|
|
79
|
+
env.CRED_USER = val;
|
|
80
|
+
env.MYSQL_USER = val;
|
|
81
|
+
env.PGUSER = val;
|
|
82
|
+
userSet = true;
|
|
83
|
+
}
|
|
84
|
+
else if (!hostSet && (lk === "host" || lk === "hostname")) {
|
|
85
|
+
env.CRED_HOST = val;
|
|
86
|
+
env.MYSQL_HOST = val;
|
|
87
|
+
env.PGHOST = val;
|
|
88
|
+
hostSet = true;
|
|
89
|
+
}
|
|
90
|
+
else if (!portSet && lk === "port") {
|
|
91
|
+
env.CRED_PORT = val;
|
|
92
|
+
env.MYSQL_TCP_PORT = val;
|
|
93
|
+
env.PGPORT = val;
|
|
94
|
+
portSet = true;
|
|
95
|
+
}
|
|
96
|
+
else if (!dbSet && (lk === "database" || lk === "db" || lk === "dbname")) {
|
|
97
|
+
env.CRED_DATABASE = val;
|
|
98
|
+
env.PGDATABASE = val;
|
|
99
|
+
dbSet = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return env;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Spawn `argv` (program + args, shell:false) with the credential env injected
|
|
106
|
+
* on top of a minimal copy of the current env. Captures stdout/stderr with a
|
|
107
|
+
* size cap and a hard timeout. Never throws on a non-zero exit; throws only on
|
|
108
|
+
* spawn failure (e.g. command not found).
|
|
109
|
+
*/
|
|
110
|
+
function runWithCredEnv(argv, credEnv, stdin) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
113
|
+
shell: false,
|
|
114
|
+
env: { ...process.env, ...credEnv },
|
|
115
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
let stdout = "";
|
|
118
|
+
let stderr = "";
|
|
119
|
+
let truncated = false;
|
|
120
|
+
let timedOut = false;
|
|
121
|
+
let settled = false;
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
timedOut = true;
|
|
124
|
+
child.kill("SIGKILL");
|
|
125
|
+
}, CRED_EXEC_TIMEOUT_MS);
|
|
126
|
+
const append = (buf, chunk) => {
|
|
127
|
+
if (buf.length >= CRED_EXEC_MAX_OUTPUT) {
|
|
128
|
+
truncated = true;
|
|
129
|
+
return buf;
|
|
130
|
+
}
|
|
131
|
+
const next = buf + chunk.toString("utf8");
|
|
132
|
+
if (next.length > CRED_EXEC_MAX_OUTPUT) {
|
|
133
|
+
truncated = true;
|
|
134
|
+
return next.slice(0, CRED_EXEC_MAX_OUTPUT);
|
|
135
|
+
}
|
|
136
|
+
return next;
|
|
137
|
+
};
|
|
138
|
+
child.stdout.on("data", (c) => { stdout = append(stdout, c); });
|
|
139
|
+
child.stderr.on("data", (c) => { stderr = append(stderr, c); });
|
|
140
|
+
child.on("error", (err) => {
|
|
141
|
+
if (settled)
|
|
142
|
+
return;
|
|
143
|
+
settled = true;
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
reject(err);
|
|
146
|
+
});
|
|
147
|
+
child.on("close", (code, signal) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
resolve({
|
|
153
|
+
stdout,
|
|
154
|
+
stderr,
|
|
155
|
+
exit_code: code,
|
|
156
|
+
signal: signal ?? null,
|
|
157
|
+
timed_out: timedOut,
|
|
158
|
+
truncated,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
if (typeof stdin === "string") {
|
|
162
|
+
child.stdin.write(stdin);
|
|
163
|
+
}
|
|
164
|
+
child.stdin.end();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
10
168
|
// Helper: 401/403 error handling
|
|
11
169
|
// ---------------------------------------------------------------------------
|
|
12
170
|
export function handleApiError(error) {
|
|
@@ -1472,11 +1630,57 @@ const handlers = {
|
|
|
1472
1630
|
try {
|
|
1473
1631
|
const ref = args.ref;
|
|
1474
1632
|
const result = await cpClient.getCredential(ref);
|
|
1633
|
+
const data = (result ?? {});
|
|
1634
|
+
// SERVER-SIDE BROKER MODEL: we never return any secret value here, and we
|
|
1635
|
+
// no longer emit shell snippets that handle the secret either. We surface
|
|
1636
|
+
// ONLY non-sensitive metadata (host/user/db/port) plus the names of the
|
|
1637
|
+
// secret fields and the env vars they will populate, and we instruct the
|
|
1638
|
+
// agent to run the command through `credential_exec`, which injects the
|
|
1639
|
+
// secret into the child process env on the server side — the value never
|
|
1640
|
+
// reaches the agent, the screen, or disk.
|
|
1641
|
+
const nonSecret = {};
|
|
1642
|
+
const secretFields = [];
|
|
1643
|
+
const credEnvKeys = Object.keys(buildCredentialEnv(data)).sort();
|
|
1644
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1645
|
+
if (typeof v === "object" && v !== null)
|
|
1646
|
+
continue;
|
|
1647
|
+
if (looksLikePassword(k))
|
|
1648
|
+
secretFields.push(k);
|
|
1649
|
+
else
|
|
1650
|
+
nonSecret[k] = v;
|
|
1651
|
+
}
|
|
1652
|
+
const hasUser = "user" in data || "username" in data;
|
|
1653
|
+
const isDbLike = hasUser && secretFields.length > 0;
|
|
1654
|
+
const lines = [];
|
|
1655
|
+
lines.push(`Credential '${ref}' resolved (metadata only — NO secret value is ever returned).`);
|
|
1656
|
+
lines.push("To USE this credential, call the `credential_exec` tool. It fetches the " +
|
|
1657
|
+
"secret on the server side, injects it into the child process environment, " +
|
|
1658
|
+
"runs your command, and returns ONLY stdout/stderr/exit_code. The secret " +
|
|
1659
|
+
"never enters this text, your context, the screen, or disk.");
|
|
1660
|
+
lines.push("");
|
|
1661
|
+
if (Object.keys(nonSecret).length > 0) {
|
|
1662
|
+
lines.push("Non-secret metadata: " + JSON.stringify(nonSecret));
|
|
1663
|
+
}
|
|
1664
|
+
if (secretFields.length > 0) {
|
|
1665
|
+
lines.push(`Secret fields (handled server-side only): ${secretFields.join(", ")}`);
|
|
1666
|
+
}
|
|
1667
|
+
lines.push(`Env vars injected into the child by credential_exec: ${credEnvKeys.join(", ")}`);
|
|
1668
|
+
lines.push("");
|
|
1669
|
+
lines.push("DO NOT request, print, or inline the secret value. Use credential_exec:");
|
|
1670
|
+
lines.push("");
|
|
1671
|
+
if (isDbLike) {
|
|
1672
|
+
lines.push("# MySQL (MYSQL_PWD / MYSQL_USER / MYSQL_HOST are pre-injected):");
|
|
1673
|
+
lines.push(`credential_exec(ref="${ref}", argv=["mysql", "-e", "SELECT 1"])`);
|
|
1674
|
+
lines.push("");
|
|
1675
|
+
lines.push("# Postgres (PGPASSWORD / PGUSER / PGHOST / PGDATABASE are pre-injected):");
|
|
1676
|
+
lines.push(`credential_exec(ref="${ref}", argv=["psql", "-c", "SELECT 1"])`);
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
lines.push("# Read an injected field inside the command (here CRED_API_KEY, etc.):");
|
|
1680
|
+
lines.push(`credential_exec(ref="${ref}", argv=["sh", "-c", "curl -sS -H \\"Authorization: Bearer $CRED_TOKEN\\" \\"$CRED_BASE_URL/whoami\\""])`);
|
|
1681
|
+
}
|
|
1475
1682
|
return {
|
|
1476
|
-
content: [{
|
|
1477
|
-
type: "text",
|
|
1478
|
-
text: JSON.stringify(result, null, 2),
|
|
1479
|
-
}],
|
|
1683
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1480
1684
|
};
|
|
1481
1685
|
}
|
|
1482
1686
|
catch (error) {
|
|
@@ -1494,6 +1698,90 @@ const handlers = {
|
|
|
1494
1698
|
throw error;
|
|
1495
1699
|
}
|
|
1496
1700
|
},
|
|
1701
|
+
// credential_exec — server-side broker. Runs a command with the credential
|
|
1702
|
+
// injected into the child process ENV. Returns ONLY stdout/stderr/exit_code.
|
|
1703
|
+
// The secret value never enters the returned text and is never logged.
|
|
1704
|
+
async credential_exec(args, { cpClient }) {
|
|
1705
|
+
const ref = args.ref;
|
|
1706
|
+
const argv = args.argv;
|
|
1707
|
+
const stdin = args.stdin;
|
|
1708
|
+
// Validate argv: must be a non-empty array of strings (program + args).
|
|
1709
|
+
// We deliberately do NOT accept a shell command string — spawn runs with
|
|
1710
|
+
// shell:false to avoid shell injection. The caller can still opt into a
|
|
1711
|
+
// shell explicitly with argv ["sh","-c","..."], which is their choice.
|
|
1712
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv.every((a) => typeof a === "string")) {
|
|
1713
|
+
return {
|
|
1714
|
+
content: [{
|
|
1715
|
+
type: "text",
|
|
1716
|
+
text: "argv must be a non-empty array of strings (program + args), e.g. [\"mysql\", \"-e\", \"SELECT 1\"]. A shell command string is not accepted (use [\"sh\", \"-c\", \"...\"] if you really need a shell).",
|
|
1717
|
+
}],
|
|
1718
|
+
isError: true,
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (stdin !== undefined && typeof stdin !== "string") {
|
|
1722
|
+
return {
|
|
1723
|
+
content: [{ type: "text", text: "stdin, if provided, must be a string." }],
|
|
1724
|
+
isError: true,
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
let credData;
|
|
1728
|
+
try {
|
|
1729
|
+
const result = await cpClient.getCredential(ref);
|
|
1730
|
+
credData = (result ?? {});
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
if (error?.response?.status === 404) {
|
|
1734
|
+
return {
|
|
1735
|
+
content: [{ type: "text", text: `No credential found for ref '${ref}'` }],
|
|
1736
|
+
isError: true,
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
const apiErr = handleApiError(error);
|
|
1740
|
+
if (apiErr)
|
|
1741
|
+
return apiErr;
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
if (Object.keys(credData).length === 0) {
|
|
1745
|
+
return {
|
|
1746
|
+
content: [{ type: "text", text: `Credential '${ref}' has no fields to inject.` }],
|
|
1747
|
+
isError: true,
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
const credEnv = buildCredentialEnv(credData);
|
|
1751
|
+
let res;
|
|
1752
|
+
try {
|
|
1753
|
+
res = await runWithCredEnv(argv, credEnv, stdin);
|
|
1754
|
+
}
|
|
1755
|
+
catch (err) {
|
|
1756
|
+
// Spawn failure (e.g. ENOENT command not found). The error message is
|
|
1757
|
+
// about the program/argv, which the caller already provided — no secret.
|
|
1758
|
+
return {
|
|
1759
|
+
content: [{
|
|
1760
|
+
type: "text",
|
|
1761
|
+
text: `Failed to execute command: ${err?.code ? err.code + " " : ""}${err?.message ?? String(err)}`,
|
|
1762
|
+
}],
|
|
1763
|
+
isError: true,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
// Return ONLY non-secret execution results. The credential value lived only
|
|
1767
|
+
// in the child's env; it is not part of stdout/stderr unless the invoked
|
|
1768
|
+
// command itself chose to print it (the caller's responsibility).
|
|
1769
|
+
const summary = [
|
|
1770
|
+
`exit_code: ${res.exit_code}`,
|
|
1771
|
+
res.signal ? `signal: ${res.signal}` : null,
|
|
1772
|
+
res.timed_out ? "timed_out: true (killed after 30s)" : null,
|
|
1773
|
+
res.truncated ? "truncated: true (output capped at 256 KiB)" : null,
|
|
1774
|
+
"",
|
|
1775
|
+
"--- stdout ---",
|
|
1776
|
+
res.stdout.length ? res.stdout : "(empty)",
|
|
1777
|
+
"--- stderr ---",
|
|
1778
|
+
res.stderr.length ? res.stderr : "(empty)",
|
|
1779
|
+
].filter((l) => l !== null).join("\n");
|
|
1780
|
+
return {
|
|
1781
|
+
content: [{ type: "text", text: summary }],
|
|
1782
|
+
isError: res.exit_code !== 0,
|
|
1783
|
+
};
|
|
1784
|
+
},
|
|
1497
1785
|
async credential_list(_args, { cpClient }) {
|
|
1498
1786
|
try {
|
|
1499
1787
|
const result = await cpClient.listCredentials();
|
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"manifest_version": "0.
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
3
|
"name": "frinus",
|
|
4
4
|
"display_name": "Frinus — Memória Cognitiva",
|
|
5
5
|
"version": "3.1.0",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"homepage": "https://frinus.rdxsec.com.br",
|
|
13
13
|
"documentation": "https://github.com/frinus-ai/frinus-mcp",
|
|
14
14
|
"support": "https://github.com/frinus-ai/frinus-mcp/issues",
|
|
15
|
-
"icon": "icon.png",
|
|
16
15
|
"server": {
|
|
17
16
|
"type": "node",
|
|
18
17
|
"entry_point": "dist/index.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frinus-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Frinus - MCP Server for Cognitive Memory (φρήν + νοῦς). Long-term memory, sessions, agents and knowledge for Claude and any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|