@cybedefend/vibedefend 1.4.7 → 1.6.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 +77 -2
- package/dist/api/gateway-request.js +49 -0
- package/dist/api/gateway-request.js.map +1 -0
- package/dist/api/projects.js +67 -0
- package/dist/api/projects.js.map +1 -1
- package/dist/api/repository-link.js +157 -0
- package/dist/api/repository-link.js.map +1 -0
- package/dist/auth/auth-store.js +107 -25
- package/dist/auth/auth-store.js.map +1 -1
- package/dist/auth/auth.js +21 -0
- package/dist/auth/auth.js.map +1 -1
- package/dist/clients/cursor.js +5 -1
- package/dist/clients/cursor.js.map +1 -1
- package/dist/clients/vscode.js +38 -29
- package/dist/clients/vscode.js.map +1 -1
- package/dist/commands/mcp/allow.js +219 -0
- package/dist/commands/mcp/allow.js.map +1 -0
- package/dist/commands/mcp/connect.js +337 -0
- package/dist/commands/mcp/connect.js.map +1 -0
- package/dist/commands/mcp/index.js +170 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/mcp/proxy.js +264 -0
- package/dist/commands/mcp/proxy.js.map +1 -0
- package/dist/commands/mcp/scan.js +349 -0
- package/dist/commands/mcp/scan.js.map +1 -0
- package/dist/commands/mcp/sync.js +215 -0
- package/dist/commands/mcp/sync.js.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostics.js +78 -12
- package/dist/diagnostics.js.map +1 -1
- package/dist/doctor.js +19 -0
- package/dist/doctor.js.map +1 -1
- package/dist/guards-evaluator/analysis-types.js +27 -0
- package/dist/guards-evaluator/analysis-types.js.map +1 -0
- package/dist/guards-evaluator/command-analysis.js +572 -0
- package/dist/guards-evaluator/command-analysis.js.map +1 -0
- package/dist/guards-evaluator/command-view.js +586 -0
- package/dist/guards-evaluator/command-view.js.map +1 -0
- package/dist/guards-evaluator/env.js +11 -0
- package/dist/guards-evaluator/env.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +91 -41
- package/dist/guards-evaluator/evaluate.js.map +1 -1
- package/dist/guards-evaluator/index.js +2 -1
- package/dist/guards-evaluator/index.js.map +1 -1
- package/dist/guards-evaluator/inline-code.js +259 -0
- package/dist/guards-evaluator/inline-code.js.map +1 -0
- package/dist/guards-evaluator/normalize.js +716 -0
- package/dist/guards-evaluator/normalize.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +13 -5
- package/dist/guards-evaluator/preprocess.js.map +1 -1
- package/dist/guards-evaluator/redact.js +19 -0
- package/dist/guards-evaluator/redact.js.map +1 -1
- package/dist/guards-evaluator/regex.js +25 -3
- package/dist/guards-evaluator/regex.js.map +1 -1
- package/dist/guards-evaluator/safe-commands.js +155 -0
- package/dist/guards-evaluator/safe-commands.js.map +1 -0
- package/dist/guards-evaluator/triage.js +315 -0
- package/dist/guards-evaluator/triage.js.map +1 -0
- package/dist/hook-runner.js +24873 -5068
- package/dist/hooks/install.js +38 -0
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/runtime/config.js +38 -0
- package/dist/hooks/runtime/config.js.map +1 -1
- package/dist/hooks/runtime/emit.js +126 -53
- package/dist/hooks/runtime/emit.js.map +1 -1
- package/dist/hooks/runtime/fetch-rules.js +27 -7
- package/dist/hooks/runtime/fetch-rules.js.map +1 -1
- package/dist/hooks/runtime/gateway-http-error.js +34 -0
- package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +139 -17
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
- package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
- package/dist/hooks/runtime/judge-cache.js +6 -0
- package/dist/hooks/runtime/judge-cache.js.map +1 -1
- package/dist/hooks/runtime/judge-triage.js +193 -0
- package/dist/hooks/runtime/judge-triage.js.map +1 -0
- package/dist/hooks/runtime/judge.js +7 -1
- package/dist/hooks/runtime/judge.js.map +1 -1
- package/dist/hooks/runtime/pre-compact.js +21 -6
- package/dist/hooks/runtime/pre-compact.js.map +1 -1
- package/dist/hooks/runtime/resolve.js +108 -17
- package/dist/hooks/runtime/resolve.js.map +1 -1
- package/dist/hooks/runtime/retry-policy.js +57 -0
- package/dist/hooks/runtime/retry-policy.js.map +1 -0
- package/dist/hooks/runtime/review-sessions.js +74 -0
- package/dist/hooks/runtime/review-sessions.js.map +1 -0
- package/dist/hooks/runtime/script-resolve.js +695 -0
- package/dist/hooks/runtime/script-resolve.js.map +1 -0
- package/dist/hooks/runtime/session-activation.js +226 -0
- package/dist/hooks/runtime/session-activation.js.map +1 -0
- package/dist/hooks/runtime/session-review.js +174 -46
- package/dist/hooks/runtime/session-review.js.map +1 -1
- package/dist/hooks/runtime/session-start.js +35 -4
- package/dist/hooks/runtime/session-start.js.map +1 -1
- package/dist/hooks/runtime/session-writes.js +232 -0
- package/dist/hooks/runtime/session-writes.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +26 -7
- package/dist/hooks/runtime/sniff.js.map +1 -1
- package/dist/hooks/runtime/target-resolve.js +336 -0
- package/dist/hooks/runtime/target-resolve.js.map +1 -0
- package/dist/hooks/runtime/unmapped-tools.js +93 -0
- package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
- package/dist/hooks/runtime/usage-block.js +4 -0
- package/dist/hooks/runtime/usage-block.js.map +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js +8 -2
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
- package/dist/hooks/runtime/wire-text.js +41 -0
- package/dist/hooks/runtime/wire-text.js.map +1 -0
- package/dist/hooks/runtime/work-dir.js +84 -0
- package/dist/hooks/runtime/work-dir.js.map +1 -0
- package/dist/index.js +45 -12
- package/dist/index.js.map +1 -1
- package/dist/install.js +174 -11
- package/dist/install.js.map +1 -1
- package/dist/link.js +74 -48
- package/dist/link.js.map +1 -1
- package/dist/login.js +99 -14
- package/dist/login.js.map +1 -1
- package/dist/mcp/auth/agent-sign-ins.js +174 -0
- package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
- package/dist/mcp/auth/callback.js +200 -0
- package/dist/mcp/auth/callback.js.map +1 -0
- package/dist/mcp/auth/connect.js +392 -0
- package/dist/mcp/auth/connect.js.map +1 -0
- package/dist/mcp/auth/discovery.js +399 -0
- package/dist/mcp/auth/discovery.js.map +1 -0
- package/dist/mcp/auth/oauth.js +326 -0
- package/dist/mcp/auth/oauth.js.map +1 -0
- package/dist/mcp/auth/probe.js +128 -0
- package/dist/mcp/auth/probe.js.map +1 -0
- package/dist/mcp/auth/secrets.js +446 -0
- package/dist/mcp/auth/secrets.js.map +1 -0
- package/dist/mcp/auth/store.js +260 -0
- package/dist/mcp/auth/store.js.map +1 -0
- package/dist/mcp/auth/tokenSource.js +55 -0
- package/dist/mcp/auth/tokenSource.js.map +1 -0
- package/dist/mcp/config/atomicWrite.js +98 -0
- package/dist/mcp/config/atomicWrite.js.map +1 -0
- package/dist/mcp/config/clients.js +171 -0
- package/dist/mcp/config/clients.js.map +1 -0
- package/dist/mcp/config/cybedefend-servers.js +48 -0
- package/dist/mcp/config/cybedefend-servers.js.map +1 -0
- package/dist/mcp/config/discover.js +114 -0
- package/dist/mcp/config/discover.js.map +1 -0
- package/dist/mcp/config/hook.js +123 -0
- package/dist/mcp/config/hook.js.map +1 -0
- package/dist/mcp/config/index.js +11 -0
- package/dist/mcp/config/index.js.map +1 -0
- package/dist/mcp/config/install.js +132 -0
- package/dist/mcp/config/install.js.map +1 -0
- package/dist/mcp/config/installed.js +37 -0
- package/dist/mcp/config/installed.js.map +1 -0
- package/dist/mcp/config/jsonEdit.js +119 -0
- package/dist/mcp/config/jsonEdit.js.map +1 -0
- package/dist/mcp/config/lock.js +102 -0
- package/dist/mcp/config/lock.js.map +1 -0
- package/dist/mcp/config/proxyBinary.js +211 -0
- package/dist/mcp/config/proxyBinary.js.map +1 -0
- package/dist/mcp/config/rewrite.js +388 -0
- package/dist/mcp/config/rewrite.js.map +1 -0
- package/dist/mcp/config/types.js +34 -0
- package/dist/mcp/config/types.js.map +1 -0
- package/dist/mcp/proxy/audit.js +83 -0
- package/dist/mcp/proxy/audit.js.map +1 -0
- package/dist/mcp/proxy/authInterceptor.js +119 -0
- package/dist/mcp/proxy/authInterceptor.js.map +1 -0
- package/dist/mcp/proxy/expand.js +93 -0
- package/dist/mcp/proxy/expand.js.map +1 -0
- package/dist/mcp/proxy/inject.js +397 -0
- package/dist/mcp/proxy/inject.js.map +1 -0
- package/dist/mcp/proxy/intercept.js +37 -0
- package/dist/mcp/proxy/intercept.js.map +1 -0
- package/dist/mcp/proxy/jsonrpc.js +153 -0
- package/dist/mcp/proxy/jsonrpc.js.map +1 -0
- package/dist/mcp/proxy/log.js +109 -0
- package/dist/mcp/proxy/log.js.map +1 -0
- package/dist/mcp/proxy/paths.js +53 -0
- package/dist/mcp/proxy/paths.js.map +1 -0
- package/dist/mcp/proxy/proxy.js +510 -0
- package/dist/mcp/proxy/proxy.js.map +1 -0
- package/dist/mcp/proxy/spec.js +203 -0
- package/dist/mcp/proxy/spec.js.map +1 -0
- package/dist/mcp/proxy/sse.js +77 -0
- package/dist/mcp/proxy/sse.js.map +1 -0
- package/dist/mcp/proxy/upstream.js +2 -0
- package/dist/mcp/proxy/upstream.js.map +1 -0
- package/dist/mcp/proxy/upstreamHttp.js +499 -0
- package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
- package/dist/mcp/proxy/upstreamStdio.js +188 -0
- package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
- package/dist/mcp/proxy/upstreams.js +143 -0
- package/dist/mcp/proxy/upstreams.js.map +1 -0
- package/dist/mcp/proxy/verifyInterceptor.js +559 -0
- package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
- package/dist/mcp/proxy/windowsCommand.js +99 -0
- package/dist/mcp/proxy/windowsCommand.js.map +1 -0
- package/dist/mcp/scan/args.js +228 -0
- package/dist/mcp/scan/args.js.map +1 -0
- package/dist/mcp/scan/results.js +253 -0
- package/dist/mcp/scan/results.js.map +1 -0
- package/dist/mcp/scan/types.js +40 -0
- package/dist/mcp/scan/types.js.map +1 -0
- package/dist/mcp/verify/api.js +475 -0
- package/dist/mcp/verify/api.js.map +1 -0
- package/dist/mcp/verify/backend.js +143 -0
- package/dist/mcp/verify/backend.js.map +1 -0
- package/dist/mcp/verify/cache.js +182 -0
- package/dist/mcp/verify/cache.js.map +1 -0
- package/dist/mcp/verify/contract-errors.js +198 -0
- package/dist/mcp/verify/contract-errors.js.map +1 -0
- package/dist/mcp/verify/digest.js +91 -0
- package/dist/mcp/verify/digest.js.map +1 -0
- package/dist/mcp/verify/factory.js +56 -0
- package/dist/mcp/verify/factory.js.map +1 -0
- package/dist/mcp/verify/identity.js +173 -0
- package/dist/mcp/verify/identity.js.map +1 -0
- package/dist/mcp/verify/policy-store.js +63 -0
- package/dist/mcp/verify/policy-store.js.map +1 -0
- package/dist/mcp/verify/policy.js +158 -0
- package/dist/mcp/verify/policy.js.map +1 -0
- package/dist/mcp/verify/pool.js +27 -0
- package/dist/mcp/verify/pool.js.map +1 -0
- package/dist/mcp/verify/token-refresh.js +44 -0
- package/dist/mcp/verify/token-refresh.js.map +1 -0
- package/dist/mcp/verify/token.js +67 -0
- package/dist/mcp/verify/token.js.map +1 -0
- package/dist/mcp/verify/verifier.js +1009 -0
- package/dist/mcp/verify/verifier.js.map +1 -0
- package/dist/org-gate.js +1 -1
- package/dist/org-gate.js.map +1 -1
- package/dist/org.js +146 -0
- package/dist/org.js.map +1 -0
- package/dist/project-create.js +129 -0
- package/dist/project-create.js.map +1 -0
- package/dist/prompts.js +21 -0
- package/dist/prompts.js.map +1 -1
- package/dist/re2.wasm +0 -0
- package/dist/status.js +61 -0
- package/dist/status.js.map +1 -1
- package/dist/uninstall.js +49 -4
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.js +62 -6
- package/dist/utils.js.map +1 -1
- package/package.json +10 -3
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials obtained from THIRD-PARTY MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* ─── Why a store of its own, rather than `src/auth/auth-store.ts` ───────────
|
|
5
|
+
*
|
|
6
|
+
* `auth-store.ts` is cut for the single CybeDefend session: its type demands
|
|
7
|
+
* `logtoEndpoint` / `cliAppId` / `logtoResource`, and its file fallback holds
|
|
8
|
+
* one entry. Here there is one credential PER MCP SERVER — a Linear token has
|
|
9
|
+
* no business at Notion.
|
|
10
|
+
*
|
|
11
|
+
* Where the bytes end up is `secrets.ts`'s problem: keychain on macOS, secret
|
|
12
|
+
* service on Linux, DPAPI on Windows, and an encrypted file when none of
|
|
13
|
+
* those answers. This file only decides what is worth keeping, when a
|
|
14
|
+
* credential is still good, and when it has to be erased.
|
|
15
|
+
*
|
|
16
|
+
* It never throws: an unreadable credential behaves like an absent one and
|
|
17
|
+
* the caller asks for a new authorization.
|
|
18
|
+
*
|
|
19
|
+
* ─── Tests never touch the real store ───────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* Every entry point takes a `backend` in its dependencies. `pnpm test` may run
|
|
22
|
+
* on a developer's machine, and a test that wrote into the real keychain
|
|
23
|
+
* during the suite would be exactly the class of damage the plan forbids for
|
|
24
|
+
* `~/.cybedefend/`, extended to the system keychain.
|
|
25
|
+
*/
|
|
26
|
+
import { mcpHomeDir } from '../proxy/paths.js';
|
|
27
|
+
import { canonicalMcpUrl, mcpAuthKey, refreshMcpToken } from './oauth.js';
|
|
28
|
+
import { selectSecretBackend, } from './secrets.js';
|
|
29
|
+
export { MCP_KEYCHAIN_SERVICE } from './secrets.js';
|
|
30
|
+
/**
|
|
31
|
+
* Choosing a backend runs a real write/read/delete round trip, so the result
|
|
32
|
+
* is kept. A proxy session reads its token once and refreshes it rarely;
|
|
33
|
+
* paying three keychain calls every time would be visible.
|
|
34
|
+
*
|
|
35
|
+
* Keyed by the directory the credentials live in, not process-wide: two calls
|
|
36
|
+
* with different environments are two different stores, and a cache that
|
|
37
|
+
* ignored that would hand the second one the first one's backend — which is
|
|
38
|
+
* a test suite quietly writing into the developer's real keychain.
|
|
39
|
+
*/
|
|
40
|
+
const selected = new Map();
|
|
41
|
+
function backendFor(deps) {
|
|
42
|
+
if (deps.backend)
|
|
43
|
+
return deps.backend;
|
|
44
|
+
const key = deps.dir ?? mcpHomeDir(deps.env ?? process.env);
|
|
45
|
+
const known = selected.get(key);
|
|
46
|
+
if (known)
|
|
47
|
+
return known.backend;
|
|
48
|
+
const chosen = selectSecretBackend(deps);
|
|
49
|
+
selected.set(key, chosen);
|
|
50
|
+
return chosen.backend;
|
|
51
|
+
}
|
|
52
|
+
/** Forgets the cached choices. For tests, and for `vibedefend doctor`. */
|
|
53
|
+
export function resetSecretBackend() {
|
|
54
|
+
selected.clear();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Persists a server's credential.
|
|
58
|
+
*
|
|
59
|
+
* Returns `false` when nothing could be written anywhere. That distinction
|
|
60
|
+
* matters: a store that silently failed would have the caller believe the
|
|
61
|
+
* authorization was kept, and the next read return `null` — the user is sent
|
|
62
|
+
* back to the browser every session with no explanation, the worst of both
|
|
63
|
+
* worlds. `secrets.ts` already announced the degradation from the platform
|
|
64
|
+
* store to the encrypted file; this is the case where even that failed.
|
|
65
|
+
*/
|
|
66
|
+
export function storeMcpAuth(serverUrl, auth, deps = {}) {
|
|
67
|
+
try {
|
|
68
|
+
backendFor(deps).set(mcpAuthKey(serverUrl), JSON.stringify(auth));
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// The error is deliberately not propagated or interpolated: its message
|
|
73
|
+
// comes from a command that was handed a token.
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function mcpClientKey(serverUrl) {
|
|
78
|
+
return `mcp-client|${canonicalMcpUrl(serverUrl) ?? serverUrl.trim().toLowerCase()}`;
|
|
79
|
+
}
|
|
80
|
+
export function storeMcpClient(serverUrl, client, deps = {}) {
|
|
81
|
+
try {
|
|
82
|
+
backendFor(deps).set(mcpClientKey(serverUrl), JSON.stringify(client));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function loadMcpClient(serverUrl, deps = {}) {
|
|
90
|
+
let blob;
|
|
91
|
+
try {
|
|
92
|
+
blob = backendFor(deps).get(mcpClientKey(serverUrl));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!blob)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(blob);
|
|
101
|
+
if (typeof parsed.clientId !== 'string' || parsed.clientId === '')
|
|
102
|
+
return null;
|
|
103
|
+
return {
|
|
104
|
+
clientId: parsed.clientId,
|
|
105
|
+
clientSecret: typeof parsed.clientSecret === 'string' ? parsed.clientSecret : null,
|
|
106
|
+
callbackPort: typeof parsed.callbackPort === 'number' ? parsed.callbackPort : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function clearMcpClient(serverUrl, deps = {}) {
|
|
114
|
+
try {
|
|
115
|
+
backendFor(deps).delete(mcpClientKey(serverUrl));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* already gone, or the store is unavailable */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** A server's credential, or `null`. */
|
|
122
|
+
export function loadMcpAuth(serverUrl, deps = {}) {
|
|
123
|
+
let blob;
|
|
124
|
+
try {
|
|
125
|
+
blob = backendFor(deps).get(mcpAuthKey(serverUrl));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!blob)
|
|
131
|
+
return null;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(blob);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (typeof parsed.accessToken !== 'string' || parsed.accessToken === '')
|
|
140
|
+
return null;
|
|
141
|
+
if (typeof parsed.tokenEndpoint !== 'string' || typeof parsed.clientId !== 'string')
|
|
142
|
+
return null;
|
|
143
|
+
// No check on `resource` here. The partitioning between servers is already
|
|
144
|
+
// structural: the key IS the canonical URL of the server, so a credential
|
|
145
|
+
// read under it was written for it. The legacy store also indexed by HOST
|
|
146
|
+
// alone, which is why it needed `resource` to prove ownership; that key is
|
|
147
|
+
// gone. Re-adding the check would reject a legitimate credential from an
|
|
148
|
+
// authorization server that names its resource differently from the MCP
|
|
149
|
+
// endpoint — `https://host/` for `https://host/mcp` is a common shape.
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Erases a server's credential.
|
|
154
|
+
*
|
|
155
|
+
* Called by `ensureMcpAccessToken` on `invalid_grant`: keeping a dead refresh
|
|
156
|
+
* token would reproduce the failure on every later call, for ever and
|
|
157
|
+
* silently. Erasing makes the missing authorization VISIBLE to the caller
|
|
158
|
+
* (`loadMcpAuth` returns `null`), which can then ask for a reconnection
|
|
159
|
+
* instead of looping.
|
|
160
|
+
*/
|
|
161
|
+
export function clearMcpAuth(serverUrl, deps = {}) {
|
|
162
|
+
try {
|
|
163
|
+
backendFor(deps).delete(mcpAuthKey(serverUrl));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* already gone, or the store is unavailable */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The access token if it is still good, or `null`. NEVER refreshes.
|
|
171
|
+
*
|
|
172
|
+
* 60 s of margin: a token that expires while the request is in flight is as
|
|
173
|
+
* useless as an expired one, and replaying it fails the probe instead of
|
|
174
|
+
* cleanly asking for a new authorization.
|
|
175
|
+
*/
|
|
176
|
+
export function validMcpAccessToken(serverUrl, deps = {}) {
|
|
177
|
+
const auth = loadMcpAuth(serverUrl, deps);
|
|
178
|
+
if (!auth)
|
|
179
|
+
return null;
|
|
180
|
+
if (auth.expiresAt && auth.expiresAt - 60_000 <= Date.now())
|
|
181
|
+
return null;
|
|
182
|
+
return auth.accessToken;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Renews the credential whatever the local clock thinks, and persists it.
|
|
186
|
+
*
|
|
187
|
+
* `ensureMcpAccessToken` only refreshes a token it believes has expired, which
|
|
188
|
+
* is the right thing on the happy path and the wrong thing after a 401: a
|
|
189
|
+
* server that revoked a token, or that expires it earlier than it advertised,
|
|
190
|
+
* leaves us holding one that looks perfectly valid here. Re-reading the store
|
|
191
|
+
* would hand back the same dead token and the retry would fail for the same
|
|
192
|
+
* reason as the first attempt.
|
|
193
|
+
*
|
|
194
|
+
* `null` means a new human authorization is needed.
|
|
195
|
+
*/
|
|
196
|
+
export async function refreshMcpAuth(serverUrl, deps = {}) {
|
|
197
|
+
const auth = loadMcpAuth(serverUrl, deps);
|
|
198
|
+
if (!auth || !auth.refreshToken)
|
|
199
|
+
return null;
|
|
200
|
+
const outcome = await (deps.refreshFn ?? refreshMcpToken)({
|
|
201
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
202
|
+
clientId: auth.clientId,
|
|
203
|
+
clientSecret: auth.clientSecret,
|
|
204
|
+
refreshToken: auth.refreshToken,
|
|
205
|
+
resource: auth.resource,
|
|
206
|
+
});
|
|
207
|
+
if (!outcome.ok) {
|
|
208
|
+
if (outcome.reason === 'invalid_grant')
|
|
209
|
+
clearMcpAuth(serverUrl, deps);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const renewed = { ...auth, ...outcome.tokens };
|
|
213
|
+
storeMcpAuth(serverUrl, renewed, deps);
|
|
214
|
+
return renewed.accessToken;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A usable access token, renewing if needed.
|
|
218
|
+
*
|
|
219
|
+
* `validMcpAccessToken` only reports the expiry; this repairs it. Without it
|
|
220
|
+
* an authorization does not survive the day and verification stops without a
|
|
221
|
+
* word — measured on a Linear token that expired within hours.
|
|
222
|
+
*
|
|
223
|
+
* `null` means a new human authorization is needed, whether nothing was
|
|
224
|
+
* stored or the renewal failed.
|
|
225
|
+
*
|
|
226
|
+
* An `invalid_grant` erases the local credential: the refresh token is dead.
|
|
227
|
+
* An `unavailable` (network, transient server failure) does NOT: the
|
|
228
|
+
* authorization is still good, only this renewal failed, and forcing a human
|
|
229
|
+
* reconnection for a network blip is a regression, not a protection. A
|
|
230
|
+
* `suspicious_redirect` is left to the caller, which must not escalate to an
|
|
231
|
+
* interactive flow against a server that just behaved that way.
|
|
232
|
+
*/
|
|
233
|
+
export async function ensureMcpAccessToken(serverUrl, deps = {}) {
|
|
234
|
+
const auth = loadMcpAuth(serverUrl, deps);
|
|
235
|
+
if (!auth)
|
|
236
|
+
return null;
|
|
237
|
+
const stillValid = !auth.expiresAt || auth.expiresAt - 60_000 > Date.now();
|
|
238
|
+
if (stillValid)
|
|
239
|
+
return auth.accessToken;
|
|
240
|
+
if (!auth.refreshToken)
|
|
241
|
+
return null;
|
|
242
|
+
const outcome = await (deps.refreshFn ?? refreshMcpToken)({
|
|
243
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
244
|
+
clientId: auth.clientId,
|
|
245
|
+
clientSecret: auth.clientSecret,
|
|
246
|
+
refreshToken: auth.refreshToken,
|
|
247
|
+
resource: auth.resource,
|
|
248
|
+
});
|
|
249
|
+
if (!outcome.ok) {
|
|
250
|
+
if (outcome.reason === 'invalid_grant')
|
|
251
|
+
clearMcpAuth(serverUrl, deps);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Persisted: otherwise every call would replay a renewal, and the NEW
|
|
255
|
+
// refresh token (rotation) would be lost at the next CLI restart.
|
|
256
|
+
const renewed = { ...auth, ...outcome.tokens };
|
|
257
|
+
storeMcpAuth(serverUrl, renewed, deps);
|
|
258
|
+
return renewed.accessToken;
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/mcp/auth/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { loadMcpAuth, refreshMcpAuth } from './store.js';
|
|
2
|
+
/** A minute of slack, the same `ensureMcpAccessToken` applies. */
|
|
3
|
+
const EXPIRY_SLACK_MS = 60_000;
|
|
4
|
+
export function createTokenSource(serverUrl, deps = {}) {
|
|
5
|
+
let cached = null;
|
|
6
|
+
let inFlight;
|
|
7
|
+
const resolve = async (previous) => {
|
|
8
|
+
let stored = null;
|
|
9
|
+
try {
|
|
10
|
+
stored = loadMcpAuth(serverUrl, deps);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
stored = null;
|
|
14
|
+
}
|
|
15
|
+
if (!stored) {
|
|
16
|
+
deps.onLookup?.('absent');
|
|
17
|
+
return { token: null, renewed: false };
|
|
18
|
+
}
|
|
19
|
+
// A read, and nothing else: the token is good as far as the local clock
|
|
20
|
+
// knows and the server has not refused it. The first 401 of every session
|
|
21
|
+
// ends here, since nothing was sent before it.
|
|
22
|
+
const stillValid = !stored.expiresAt || stored.expiresAt - EXPIRY_SLACK_MS > Date.now();
|
|
23
|
+
if (stillValid && stored.accessToken !== previous) {
|
|
24
|
+
deps.onLookup?.('found');
|
|
25
|
+
return { token: stored.accessToken, renewed: false };
|
|
26
|
+
}
|
|
27
|
+
// Either the local clock says it expired, or the store handed back the
|
|
28
|
+
// very token the server just refused — a server-side revocation, or an
|
|
29
|
+
// expiry earlier than advertised. Both are repaired the same way, and
|
|
30
|
+
// both are renewals worth the name.
|
|
31
|
+
const refreshed = await refreshMcpAuth(serverUrl, deps).catch(() => null);
|
|
32
|
+
deps.onLookup?.(refreshed ? 'refreshed' : 'absent');
|
|
33
|
+
return { token: refreshed, renewed: refreshed !== null };
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
async token() {
|
|
37
|
+
return cached;
|
|
38
|
+
},
|
|
39
|
+
async reauthorize(previous) {
|
|
40
|
+
// Several requests can hit a 401 at once; one lookup is enough for all.
|
|
41
|
+
if (!inFlight) {
|
|
42
|
+
inFlight = resolve(previous)
|
|
43
|
+
.then((outcome) => {
|
|
44
|
+
cached = outcome.token;
|
|
45
|
+
return outcome;
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
inFlight = undefined;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return inFlight;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=tokenSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenSource.js","sourceRoot":"","sources":["../../../src/mcp/auth/tokenSource.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAwB,MAAM,YAAY,CAAC;AAoB/E,kEAAkE;AAClE,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,OAAwB,EAAE;IAC7E,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,QAA8C,CAAC;IAEnD,MAAM,OAAO,GAAG,KAAK,EAAE,QAAuB,EAA4B,EAAE;QAC1E,IAAI,MAAM,GAAmC,IAAI,CAAC;QAClD,IAAI,CAAC;YACH,MAAM,GAAG,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;YAC1B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,CAAC;QAED,wEAAwE;QACxE,0EAA0E;QAC1E,+CAA+C;QAC/C,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACxF,IAAI,UAAU,IAAI,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;YAClD,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC;YACzB,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACvD,CAAC;QAED,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,oCAAoC;QACpC,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC1E,IAAI,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,KAAK,IAAI,EAAE,CAAC;IAC3D,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,KAAK;YACT,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,QAAuB;YACvC,wEAAwE;YACxE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;qBACzB,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;oBAChB,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;oBACvB,OAAO,OAAO,CAAC;gBACjB,CAAC,CAAC;qBACD,OAAO,CAAC,GAAG,EAAE;oBACZ,QAAQ,GAAG,SAAS,CAAC;gBACvB,CAAC,CAAC,CAAC;YACP,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replacing a file without ever letting a reader see half of it.
|
|
3
|
+
*
|
|
4
|
+
* Write to a sibling, then `rename`. The rename is atomic on every filesystem
|
|
5
|
+
* we ship to, so a client reading `~/.claude.json` at the moment we replace it
|
|
6
|
+
* sees the old document or the new one, never a truncated one — and a crash
|
|
7
|
+
* mid-write leaves the original intact.
|
|
8
|
+
*
|
|
9
|
+
* Two things the naive version gets wrong:
|
|
10
|
+
*
|
|
11
|
+
* - **symlinks.** A `~/.claude.json` that is a link into a dotfiles
|
|
12
|
+
* repository is a common setup, and `rename` onto the link REPLACES THE
|
|
13
|
+
* LINK with a regular file: the repository stops being the source of
|
|
14
|
+
* truth, silently, and the next `git pull` in it does nothing visible.
|
|
15
|
+
* Resolving the target first means we replace what the link points at,
|
|
16
|
+
* which is what the user meant.
|
|
17
|
+
* - **Windows.** `rename` fails with `EBUSY` or `EPERM` when an antivirus or
|
|
18
|
+
* an editor has the target open for a few milliseconds. It is transient
|
|
19
|
+
* and common enough that giving up on the first one loses writes on
|
|
20
|
+
* exactly one platform.
|
|
21
|
+
*/
|
|
22
|
+
import { lstatSync, mkdirSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { dirname, resolve } from 'node:path';
|
|
24
|
+
/** Attempts, and the pause between them, for the Windows `EBUSY` window. */
|
|
25
|
+
export const RENAME_ATTEMPTS = 5;
|
|
26
|
+
const RENAME_PAUSE_MS = 20;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves what `path` really points at.
|
|
29
|
+
*
|
|
30
|
+
* Three cases. An ordinary file resolves to itself. A link resolves to its
|
|
31
|
+
* target. And a DANGLING link — a dotfiles repository that has not been
|
|
32
|
+
* cloned yet — resolves to where it points, which is what `> link` does in a
|
|
33
|
+
* shell: the target gets created and the link starts working. Replacing the
|
|
34
|
+
* link with a regular file instead would quietly take the path out of the
|
|
35
|
+
* repository for good.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveWriteTarget(path) {
|
|
38
|
+
try {
|
|
39
|
+
return realpathSync(path);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* it does not exist, or it is a dangling link; both are handled below */
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
46
|
+
return resolve(dirname(path), readlinkSync(path));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* not a link either: the caller is creating the file */
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
export function writeFileAtomic(path, content, options = {}) {
|
|
55
|
+
const target = resolveWriteTarget(path);
|
|
56
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
57
|
+
mkdirSync(dirname(target), { recursive: true, mode: options.directoryMode ?? 0o700 });
|
|
58
|
+
writeFileSync(temporary, content, { encoding: 'utf8', mode: options.mode ?? 0o600 });
|
|
59
|
+
try {
|
|
60
|
+
renameWithRetry(temporary, target, options.sleep ?? busyWait, options.renameFn ?? renameSync);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(temporary);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* nothing to clean up */
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function renameWithRetry(from, to, sleep, rename) {
|
|
73
|
+
for (let attempt = 1;; attempt += 1) {
|
|
74
|
+
try {
|
|
75
|
+
rename(from, to);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const code = error.code;
|
|
80
|
+
const transient = code === 'EBUSY' || code === 'EPERM' || code === 'EACCES';
|
|
81
|
+
if (!transient || attempt >= RENAME_ATTEMPTS)
|
|
82
|
+
throw error;
|
|
83
|
+
sleep(RENAME_PAUSE_MS * attempt);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Synchronous on purpose: every caller here is synchronous, and making them
|
|
89
|
+
* async to sleep properly would turn a config write into something a caller
|
|
90
|
+
* could forget to await. The total wait is bounded at 300 ms.
|
|
91
|
+
*/
|
|
92
|
+
function busyWait(ms) {
|
|
93
|
+
const until = Date.now() + ms;
|
|
94
|
+
while (Date.now() < until) {
|
|
95
|
+
/* spin */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=atomicWrite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomicWrite.js","sourceRoot":"","sources":["../../../src/mcp/config/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,MAAM,eAAe,GAAG,EAAE,CAAC;AAa3B;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;IACD,IAAI,CAAC;QACH,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;YACrC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,OAAe,EACf,UAA8B,EAAE;IAEhC,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,GAAG,MAAM,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;IAE/D,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,IAAI,KAAK,EAAE,CAAC,CAAC;IACtF,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;IAErF,IAAI,CAAC;QACH,eAAe,CACb,SAAS,EACT,MAAM,EACN,OAAO,CAAC,KAAK,IAAI,QAAQ,EACzB,OAAO,CAAC,QAAQ,IAAI,UAAU,CAC/B,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,UAAU,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CACtB,IAAY,EACZ,EAAU,EACV,KAA2B,EAC3B,MAA0C;IAE1C,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACjB,OAAO;QACT,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;YACnD,MAAM,SAAS,GAAG,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,QAAQ,CAAC;YAC5E,IAAI,CAAC,SAAS,IAAI,OAAO,IAAI,eAAe;gBAAE,MAAM,KAAK,CAAC;YAC1D,KAAK,CAAC,eAAe,GAAG,OAAO,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,EAAU;IAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;IAC9B,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;QAC1B,UAAU;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five clients, one adapter each.
|
|
3
|
+
*
|
|
4
|
+
* They differ in three ways and no more: where the file lives, what the map
|
|
5
|
+
* of servers is called inside it, and how an HTTP server is spelled. Keeping
|
|
6
|
+
* them in one file makes those three differences readable side by side, which
|
|
7
|
+
* is the only reason anyone opens this module.
|
|
8
|
+
*
|
|
9
|
+
* The HTTP spellings, measured against what each client actually writes:
|
|
10
|
+
*
|
|
11
|
+
* Claude Code {"type":"http","url":…,"headers":{…}}
|
|
12
|
+
* Claude Desktop stdio only — it spawns `mcp-remote` for remote servers
|
|
13
|
+
* Cursor {"url":…,"headers":{…}} (no `type`)
|
|
14
|
+
* VS Code {"type":"http","url":…} (map is `servers`, not `mcpServers`)
|
|
15
|
+
* Windsurf {"serverUrl":…} (its own key)
|
|
16
|
+
*/
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { clientConfigRoot, clientConfigRootIsOverridden } from '../proxy/paths.js';
|
|
19
|
+
import { wrappedEntryFor, } from './types.js';
|
|
20
|
+
/** The home directory, or `VIBEDEFEND_MCP_DIR` standing in for it (see `paths.ts`). */
|
|
21
|
+
function home(env) {
|
|
22
|
+
return clientConfigRoot(env);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Where a VS Code-family editor keeps its user profile.
|
|
26
|
+
*
|
|
27
|
+
* The same layout serves Claude Desktop, which is why it is shared: macOS
|
|
28
|
+
* puts it under `Application Support`, Windows under `APPDATA`, Linux under
|
|
29
|
+
* `XDG_CONFIG_HOME` or `~/.config`. When `VIBEDEFEND_MCP_DIR` stands in for
|
|
30
|
+
* the home directory, `APPDATA` and `XDG_CONFIG_HOME` are ignored too: they
|
|
31
|
+
* would point back at the real profile.
|
|
32
|
+
*/
|
|
33
|
+
function applicationSupport(env, platform, app) {
|
|
34
|
+
const isolated = clientConfigRootIsOverridden(env);
|
|
35
|
+
if (platform === 'darwin')
|
|
36
|
+
return join(home(env), 'Library', 'Application Support', app);
|
|
37
|
+
if (platform === 'win32') {
|
|
38
|
+
const roaming = !isolated && env.APPDATA ? env.APPDATA : join(home(env), 'AppData', 'Roaming');
|
|
39
|
+
return join(roaming, app);
|
|
40
|
+
}
|
|
41
|
+
const xdg = !isolated && env.XDG_CONFIG_HOME ? env.XDG_CONFIG_HOME : join(home(env), '.config');
|
|
42
|
+
return join(xdg, app);
|
|
43
|
+
}
|
|
44
|
+
/** `{command, args, env, cwd}`, the shape every client accepts. */
|
|
45
|
+
function parseStdio(raw) {
|
|
46
|
+
const command = typeof raw.command === 'string' ? raw.command : '';
|
|
47
|
+
if (command === '')
|
|
48
|
+
return undefined;
|
|
49
|
+
return {
|
|
50
|
+
transport: 'stdio',
|
|
51
|
+
command,
|
|
52
|
+
args: Array.isArray(raw.args) ? raw.args.map((arg) => String(arg)) : [],
|
|
53
|
+
env: stringMap(raw.env),
|
|
54
|
+
cwd: typeof raw.cwd === 'string' ? raw.cwd : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseHttp(url, headers) {
|
|
58
|
+
if (typeof url !== 'string' || !/^https?:\/\//i.test(url))
|
|
59
|
+
return undefined;
|
|
60
|
+
return { transport: 'http', url, headers: stringMap(headers) };
|
|
61
|
+
}
|
|
62
|
+
function stringMap(value) {
|
|
63
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
64
|
+
return undefined;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
67
|
+
if (typeof entry === 'string')
|
|
68
|
+
out[key] = entry;
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
71
|
+
}
|
|
72
|
+
function classify(spec) {
|
|
73
|
+
if (!spec)
|
|
74
|
+
return { kind: 'unknown' };
|
|
75
|
+
return { kind: spec.transport, spec };
|
|
76
|
+
}
|
|
77
|
+
/** The common case: one user-level file with an `mcpServers` object. */
|
|
78
|
+
function simpleAdapter(options) {
|
|
79
|
+
return {
|
|
80
|
+
id: options.id,
|
|
81
|
+
label: options.label,
|
|
82
|
+
locations: (env, platform) => [
|
|
83
|
+
{ client: options.id, file: options.file(env, platform), scope: 'user' },
|
|
84
|
+
],
|
|
85
|
+
serverMaps: () => [{ keys: [options.mapKey] }],
|
|
86
|
+
parse: (_name, raw) => classify(options.parse(raw)),
|
|
87
|
+
wrappedEntry: (upstreamName) => wrappedEntryFor(upstreamName),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export const claudeCodeAdapter = {
|
|
91
|
+
id: 'claude-code',
|
|
92
|
+
label: 'Claude Code',
|
|
93
|
+
locations: (env) => [{ client: 'claude-code', file: join(home(env), '.claude.json'), scope: 'user' }],
|
|
94
|
+
/**
|
|
95
|
+
* Two levels in one file. `mcpServers` at the root is what
|
|
96
|
+
* `claude mcp add --scope user` writes; `projects.<path>.mcpServers` is
|
|
97
|
+
* what the default `--scope local` writes, and it is visible only from that
|
|
98
|
+
* directory. BOTH are discovered and BOTH are rewritten: they live in the
|
|
99
|
+
* same user-owned file, and the second is where `claude mcp add` writes by
|
|
100
|
+
* default (see `McpConfigScope`, amended 2026-09-09).
|
|
101
|
+
*/
|
|
102
|
+
serverMaps: (document, location) => {
|
|
103
|
+
const maps = [{ keys: ['mcpServers'] }];
|
|
104
|
+
const projects = document.value.projects;
|
|
105
|
+
if (typeof projects === 'object' && projects !== null && !Array.isArray(projects)) {
|
|
106
|
+
for (const projectPath of Object.keys(projects)) {
|
|
107
|
+
maps.push({ keys: ['projects', projectPath, 'mcpServers'] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
void location;
|
|
111
|
+
return maps;
|
|
112
|
+
},
|
|
113
|
+
parse: (_name, raw) => classify(raw.type === 'http' || raw.type === 'sse' || typeof raw.url === 'string'
|
|
114
|
+
? parseHttp(raw.url, raw.headers)
|
|
115
|
+
: parseStdio(raw)),
|
|
116
|
+
wrappedEntry: (upstreamName) => wrappedEntryFor(upstreamName),
|
|
117
|
+
};
|
|
118
|
+
export const cursorAdapter = simpleAdapter({
|
|
119
|
+
id: 'cursor',
|
|
120
|
+
label: 'Cursor',
|
|
121
|
+
file: (env) => join(home(env), '.cursor', 'mcp.json'),
|
|
122
|
+
mapKey: 'mcpServers',
|
|
123
|
+
// Cursor tells the two apart by the presence of `url`, not by a `type`.
|
|
124
|
+
parse: (raw) => (typeof raw.url === 'string' ? parseHttp(raw.url, raw.headers) : parseStdio(raw)),
|
|
125
|
+
});
|
|
126
|
+
export const windsurfAdapter = simpleAdapter({
|
|
127
|
+
id: 'windsurf',
|
|
128
|
+
label: 'Windsurf',
|
|
129
|
+
file: (env) => join(home(env), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
130
|
+
mapKey: 'mcpServers',
|
|
131
|
+
parse: (raw) => typeof raw.serverUrl === 'string'
|
|
132
|
+
? parseHttp(raw.serverUrl, raw.headers)
|
|
133
|
+
: typeof raw.url === 'string'
|
|
134
|
+
? parseHttp(raw.url, raw.headers)
|
|
135
|
+
: parseStdio(raw),
|
|
136
|
+
});
|
|
137
|
+
export const vscodeAdapter = simpleAdapter({
|
|
138
|
+
id: 'vscode',
|
|
139
|
+
label: 'VS Code',
|
|
140
|
+
file: (env, platform) => join(applicationSupport(env, platform, 'Code'), 'User', 'mcp.json'),
|
|
141
|
+
// `servers`, NOT `mcpServers`. Getting this wrong reads an empty file and
|
|
142
|
+
// reports "no MCP server configured" on a machine full of them.
|
|
143
|
+
mapKey: 'servers',
|
|
144
|
+
parse: (raw) => (typeof raw.url === 'string' ? parseHttp(raw.url, raw.headers) : parseStdio(raw)),
|
|
145
|
+
});
|
|
146
|
+
export const claudeDesktopAdapter = simpleAdapter({
|
|
147
|
+
id: 'claude-desktop',
|
|
148
|
+
label: 'Claude Desktop',
|
|
149
|
+
file: (env, platform) => join(applicationSupport(env, platform, 'Claude'), 'claude_desktop_config.json'),
|
|
150
|
+
mapKey: 'mcpServers',
|
|
151
|
+
// Claude Desktop has no HTTP transport: a remote server is configured as a
|
|
152
|
+
// command that spawns `mcp-remote`, so this is stdio-only on purpose.
|
|
153
|
+
parse: (raw) => parseStdio(raw),
|
|
154
|
+
});
|
|
155
|
+
export const MCP_CLIENT_ADAPTERS = [
|
|
156
|
+
claudeCodeAdapter,
|
|
157
|
+
cursorAdapter,
|
|
158
|
+
vscodeAdapter,
|
|
159
|
+
windsurfAdapter,
|
|
160
|
+
claudeDesktopAdapter,
|
|
161
|
+
];
|
|
162
|
+
export function mcpAdapterFor(id) {
|
|
163
|
+
const adapter = MCP_CLIENT_ADAPTERS.find((candidate) => candidate.id === id);
|
|
164
|
+
if (!adapter)
|
|
165
|
+
throw new Error(`no MCP client adapter for "${id}"`);
|
|
166
|
+
return adapter;
|
|
167
|
+
}
|
|
168
|
+
export function isMcpClientId(value) {
|
|
169
|
+
return MCP_CLIENT_ADAPTERS.some((adapter) => adapter.id === value);
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","sourceRoot":"","sources":["../../../src/mcp/config/clients.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CybeDefend's own MCP servers, which the proxy never wraps.
|
|
3
|
+
*
|
|
4
|
+
* Wrapping one bought nothing (its tools would be checked against itself) and
|
|
5
|
+
* cost a second OAuth sign-in, without which the agent lost the CybeDefend
|
|
6
|
+
* tools; and `vibedefend install` manages that entry itself, so each install
|
|
7
|
+
* wrote it back in clear and the next sync wrapped it again under another
|
|
8
|
+
* name.
|
|
9
|
+
*
|
|
10
|
+
* Trusted sources only, as `trustedMcpNames` in `uninstall.ts`: the built-in
|
|
11
|
+
* regions, the region recorded at install time and the user-global
|
|
12
|
+
* ~/.cybedefend/regions.json. Never the `vibedefend.regions.json` of the
|
|
13
|
+
* working directory: a cloned repository could list any server there and
|
|
14
|
+
* take it out of the proxy.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { sameMcpUrl } from '../../clients/mcp-url.js';
|
|
20
|
+
import { REGIONS } from '../../config.js';
|
|
21
|
+
function readJsonQuiet(path) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The MCP URLs of every CybeDefend region this machine trusts. */
|
|
30
|
+
export function cybedefendMcpUrls(env = process.env) {
|
|
31
|
+
const dir = join(env.CYBEDEFEND_HOME ?? env.HOME ?? homedir(), '.cybedefend');
|
|
32
|
+
const urls = REGIONS.map((region) => region.mcpUrl);
|
|
33
|
+
const runtime = readJsonQuiet(join(dir, 'runtime-config.json'));
|
|
34
|
+
urls.push(runtime?.region?.mcpUrl);
|
|
35
|
+
const userRegions = readJsonQuiet(join(dir, 'regions.json'));
|
|
36
|
+
if (Array.isArray(userRegions)) {
|
|
37
|
+
for (const region of userRegions) {
|
|
38
|
+
if (region && typeof region === 'object')
|
|
39
|
+
urls.push(region.mcpUrl);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return urls.filter((url) => typeof url === 'string' && url.trim() !== '');
|
|
43
|
+
}
|
|
44
|
+
/** Is `url` one of CybeDefend's own MCP servers? */
|
|
45
|
+
export function isCybeDefendMcpUrl(url, env = process.env) {
|
|
46
|
+
return cybedefendMcpUrls(env).some((known) => sameMcpUrl(known, url));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=cybedefend-servers.js.map
|