fix(mcp-server): hydrate model credentials into env
This commit is contained in:
parent
f886cf01c2
commit
f350647863
5 changed files with 65 additions and 18 deletions
|
|
@ -148,7 +148,7 @@ Recommended verification order:
|
|||
|
||||
- Use absolute paths for local executables and scripts when possible.
|
||||
- For `stdio` servers, prefer setting required environment variables directly in the MCP config instead of relying on an interactive shell profile.
|
||||
- GSD and `gsd-mcp-server` both hydrate supported tool keys saved in `~/.gsd/agent/auth.json`, so MCP configs can safely reference them through `${ENV_VAR}` placeholders without committing raw credentials.
|
||||
- GSD and `gsd-mcp-server` both hydrate supported model and tool keys saved in `~/.gsd/agent/auth.json`, so MCP configs can safely reference them through `${ENV_VAR}` placeholders without committing raw credentials.
|
||||
- If a server is team-shared and safe to commit, `.mcp.json` is usually the better home.
|
||||
- If a server depends on machine-local paths, personal services, or local-only secrets, prefer `.gsd/mcp.json`.
|
||||
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ Resolve a pending blocker in a session by sending a response to the blocked UI r
|
|||
| `GSD_CLI_PATH` | Absolute path to the GSD CLI binary. If not set, the server resolves `gsd` via `which`. |
|
||||
| `GSD_WORKFLOW_EXECUTORS_MODULE` | Optional absolute path or `file:` URL for the shared GSD workflow executor module used by workflow mutation tools. |
|
||||
|
||||
The server also hydrates supported tool credentials from `~/.gsd/agent/auth.json` on startup. Keys saved through `/gsd config` become available to the MCP server process automatically, and any explicitly-set environment variable still wins.
|
||||
The server also hydrates supported model-provider and tool credentials from `~/.gsd/agent/auth.json` on startup. Keys saved through `/gsd config` or `/gsd keys` become available to the MCP server process automatically, and any explicitly-set environment variable still wins.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import { SessionManager } from './session-manager.js';
|
||||
import { createMcpServer } from './server.js';
|
||||
import { loadStoredToolEnvKeys } from './tool-credentials.js';
|
||||
import { loadStoredCredentialEnvKeys } from './tool-credentials.js';
|
||||
|
||||
const MCP_PKG = '@modelcontextprotocol/sdk';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
loadStoredToolEnvKeys();
|
||||
loadStoredCredentialEnvKeys();
|
||||
|
||||
const sessionManager = new SessionManager();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,26 +4,33 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { loadStoredToolEnvKeys, resolveAuthPath } from "./tool-credentials.js";
|
||||
import { loadStoredCredentialEnvKeys, resolveAuthPath } from "./tool-credentials.js";
|
||||
|
||||
describe("tool credentials", () => {
|
||||
it("hydrates supported keys from auth.json", () => {
|
||||
it("hydrates supported model and tool keys from auth.json", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-auth-"));
|
||||
const authPath = join(tempRoot, "auth.json");
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
anthropic: { type: "api_key", key: "sk-ant-secret" },
|
||||
openai: { type: "api_key", key: "sk-openai-secret" },
|
||||
tavily: { type: "api_key", key: "tvly-secret" },
|
||||
context7: [{ type: "api_key", key: "ctx7-secret" }],
|
||||
anthropic: { type: "api_key", key: "sk-ant-ignore" },
|
||||
}));
|
||||
|
||||
const loaded = loadStoredToolEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded.sort(), ["CONTEXT7_API_KEY", "TAVILY_API_KEY"]);
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded.sort(), [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CONTEXT7_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
]);
|
||||
assert.equal(env.ANTHROPIC_API_KEY, "sk-ant-secret");
|
||||
assert.equal(env.OPENAI_API_KEY, "sk-openai-secret");
|
||||
assert.equal(env.TAVILY_API_KEY, "tvly-secret");
|
||||
assert.equal(env.CONTEXT7_API_KEY, "ctx7-secret");
|
||||
assert.equal(env.ANTHROPIC_API_KEY, undefined);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -39,11 +46,33 @@ describe("tool credentials", () => {
|
|||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
brave: { type: "api_key", key: "from-auth-json" },
|
||||
anthropic: { type: "api_key", key: "sk-ant-from-auth-json" },
|
||||
}));
|
||||
|
||||
const loaded = loadStoredToolEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded, []);
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded, ["ANTHROPIC_API_KEY"]);
|
||||
assert.equal(env.BRAVE_API_KEY, "already-set");
|
||||
assert.equal(env.ANTHROPIC_API_KEY, "sk-ant-from-auth-json");
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores oauth credentials because they are resolved through auth storage, not env hydration", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-auth-"));
|
||||
const authPath = join(tempRoot, "auth.json");
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
openai: { type: "oauth", access: "oauth-access-token" },
|
||||
"google-gemini-cli": { type: "oauth", token: "ya29.oauth-token" },
|
||||
}));
|
||||
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded, []);
|
||||
assert.equal(env.OPENAI_API_KEY, undefined);
|
||||
assert.equal(env.GEMINI_API_KEY, undefined);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,28 @@ type AuthCredential =
|
|||
|
||||
type AuthStorageData = Record<string, AuthCredential>;
|
||||
|
||||
const TOOL_ENV_KEYS = [
|
||||
const AUTH_ENV_KEYS = [
|
||||
["anthropic", "ANTHROPIC_API_KEY"],
|
||||
["openai", "OPENAI_API_KEY"],
|
||||
["github-copilot", "GITHUB_TOKEN"],
|
||||
["google", "GEMINI_API_KEY"],
|
||||
["groq", "GROQ_API_KEY"],
|
||||
["xai", "XAI_API_KEY"],
|
||||
["openrouter", "OPENROUTER_API_KEY"],
|
||||
["mistral", "MISTRAL_API_KEY"],
|
||||
["ollama-cloud", "OLLAMA_API_KEY"],
|
||||
["custom-openai", "CUSTOM_OPENAI_API_KEY"],
|
||||
["cerebras", "CEREBRAS_API_KEY"],
|
||||
["azure-openai-responses", "AZURE_OPENAI_API_KEY"],
|
||||
["vercel-ai-gateway", "AI_GATEWAY_API_KEY"],
|
||||
["zai", "ZAI_API_KEY"],
|
||||
["minimax", "MINIMAX_API_KEY"],
|
||||
["minimax-cn", "MINIMAX_CN_API_KEY"],
|
||||
["huggingface", "HF_TOKEN"],
|
||||
["opencode", "OPENCODE_API_KEY"],
|
||||
["opencode-go", "OPENCODE_API_KEY"],
|
||||
["kimi-coding", "KIMI_API_KEY"],
|
||||
["alibaba-coding-plan", "ALIBABA_API_KEY"],
|
||||
["brave", "BRAVE_API_KEY"],
|
||||
["brave_answers", "BRAVE_ANSWERS_KEY"],
|
||||
["context7", "CONTEXT7_API_KEY"],
|
||||
|
|
@ -17,9 +38,6 @@ const TOOL_ENV_KEYS = [
|
|||
["slack_bot", "SLACK_BOT_TOKEN"],
|
||||
["discord_bot", "DISCORD_BOT_TOKEN"],
|
||||
["telegram_bot", "TELEGRAM_BOT_TOKEN"],
|
||||
["groq", "GROQ_API_KEY"],
|
||||
["ollama-cloud", "OLLAMA_API_KEY"],
|
||||
["custom-openai", "CUSTOM_OPENAI_API_KEY"],
|
||||
] as const;
|
||||
|
||||
function expandHome(pathValue: string): string {
|
||||
|
|
@ -48,7 +66,7 @@ export function resolveAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
|||
return join(homedir(), ".gsd", "agent", "auth.json");
|
||||
}
|
||||
|
||||
export function loadStoredToolEnvKeys(options: {
|
||||
export function loadStoredCredentialEnvKeys(options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
authPath?: string;
|
||||
} = {}): string[] {
|
||||
|
|
@ -67,7 +85,7 @@ export function loadStoredToolEnvKeys(options: {
|
|||
}
|
||||
|
||||
const loaded: string[] = [];
|
||||
for (const [providerId, envVar] of TOOL_ENV_KEYS) {
|
||||
for (const [providerId, envVar] of AUTH_ENV_KEYS) {
|
||||
if (env[envVar]) continue;
|
||||
const key = getStoredApiKey(parsed, providerId);
|
||||
if (!key) continue;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue