@getaegis/cli 0.8.0 → 0.9.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.
Files changed (242) hide show
  1. package/README.md +43 -14
  2. package/dist/agent/agent.d.ts +98 -0
  3. package/dist/agent/agent.d.ts.map +1 -0
  4. package/dist/agent/agent.js +212 -0
  5. package/dist/agent/agent.js.map +1 -0
  6. package/dist/agent/index.d.ts +3 -0
  7. package/dist/agent/index.d.ts.map +1 -0
  8. package/dist/agent/index.js +2 -0
  9. package/dist/agent/index.js.map +1 -0
  10. package/dist/cli/auth.d.ts +19 -0
  11. package/dist/cli/auth.d.ts.map +1 -0
  12. package/dist/cli/auth.js +44 -0
  13. package/dist/cli/auth.js.map +1 -0
  14. package/dist/cli/commands/agent.d.ts +6 -0
  15. package/dist/cli/commands/agent.d.ts.map +1 -0
  16. package/dist/cli/commands/agent.js +241 -0
  17. package/dist/cli/commands/agent.js.map +1 -0
  18. package/dist/cli/commands/config.d.ts +6 -0
  19. package/dist/cli/commands/config.d.ts.map +1 -0
  20. package/dist/cli/commands/config.js +125 -0
  21. package/dist/cli/commands/config.js.map +1 -0
  22. package/dist/cli/commands/dashboard.d.ts +6 -0
  23. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  24. package/dist/cli/commands/dashboard.js +195 -0
  25. package/dist/cli/commands/dashboard.js.map +1 -0
  26. package/dist/cli/commands/db.d.ts +6 -0
  27. package/dist/cli/commands/db.d.ts.map +1 -0
  28. package/dist/cli/commands/db.js +139 -0
  29. package/dist/cli/commands/db.js.map +1 -0
  30. package/dist/cli/commands/doctor.d.ts +6 -0
  31. package/dist/cli/commands/doctor.d.ts.map +1 -0
  32. package/dist/cli/commands/doctor.js +39 -0
  33. package/dist/cli/commands/doctor.js.map +1 -0
  34. package/dist/cli/commands/gate.d.ts +6 -0
  35. package/dist/cli/commands/gate.d.ts.map +1 -0
  36. package/dist/cli/commands/gate.js +202 -0
  37. package/dist/cli/commands/gate.js.map +1 -0
  38. package/dist/cli/commands/init.d.ts +6 -0
  39. package/dist/cli/commands/init.d.ts.map +1 -0
  40. package/dist/cli/commands/init.js +175 -0
  41. package/dist/cli/commands/init.js.map +1 -0
  42. package/dist/cli/commands/key.d.ts +6 -0
  43. package/dist/cli/commands/key.d.ts.map +1 -0
  44. package/dist/cli/commands/key.js +49 -0
  45. package/dist/cli/commands/key.js.map +1 -0
  46. package/dist/cli/commands/ledger.d.ts +6 -0
  47. package/dist/cli/commands/ledger.d.ts.map +1 -0
  48. package/dist/cli/commands/ledger.js +140 -0
  49. package/dist/cli/commands/ledger.js.map +1 -0
  50. package/dist/cli/commands/mcp.d.ts +6 -0
  51. package/dist/cli/commands/mcp.d.ts.map +1 -0
  52. package/dist/cli/commands/mcp.js +224 -0
  53. package/dist/cli/commands/mcp.js.map +1 -0
  54. package/dist/cli/commands/policy.d.ts +6 -0
  55. package/dist/cli/commands/policy.d.ts.map +1 -0
  56. package/dist/cli/commands/policy.js +126 -0
  57. package/dist/cli/commands/policy.js.map +1 -0
  58. package/dist/cli/commands/user.d.ts +6 -0
  59. package/dist/cli/commands/user.d.ts.map +1 -0
  60. package/dist/cli/commands/user.js +150 -0
  61. package/dist/cli/commands/user.js.map +1 -0
  62. package/dist/cli/commands/vault-manager.d.ts +6 -0
  63. package/dist/cli/commands/vault-manager.d.ts.map +1 -0
  64. package/dist/cli/commands/vault-manager.js +240 -0
  65. package/dist/cli/commands/vault-manager.js.map +1 -0
  66. package/dist/cli/commands/vault.d.ts +6 -0
  67. package/dist/cli/commands/vault.d.ts.map +1 -0
  68. package/dist/cli/commands/vault.js +265 -0
  69. package/dist/cli/commands/vault.js.map +1 -0
  70. package/dist/cli/commands/webhook.d.ts +6 -0
  71. package/dist/cli/commands/webhook.d.ts.map +1 -0
  72. package/dist/cli/commands/webhook.js +151 -0
  73. package/dist/cli/commands/webhook.js.map +1 -0
  74. package/dist/cli/helpers.d.ts +12 -0
  75. package/dist/cli/helpers.d.ts.map +1 -0
  76. package/dist/cli/helpers.js +61 -0
  77. package/dist/cli/helpers.js.map +1 -0
  78. package/dist/cli/index.d.ts +19 -0
  79. package/dist/cli/index.d.ts.map +1 -0
  80. package/dist/cli/index.js +19 -0
  81. package/dist/cli/index.js.map +1 -0
  82. package/dist/cli/validation.d.ts +37 -0
  83. package/dist/cli/validation.d.ts.map +1 -0
  84. package/dist/cli/validation.js +104 -0
  85. package/dist/cli/validation.js.map +1 -0
  86. package/dist/cli.d.ts +3 -0
  87. package/dist/cli.d.ts.map +1 -0
  88. package/dist/cli.js +37 -0
  89. package/dist/cli.js.map +1 -0
  90. package/dist/config.d.ts +120 -0
  91. package/dist/config.d.ts.map +1 -0
  92. package/dist/config.js +401 -0
  93. package/dist/config.js.map +1 -0
  94. package/dist/dashboard/dashboard-server.d.ts +95 -0
  95. package/dist/dashboard/dashboard-server.d.ts.map +1 -0
  96. package/dist/dashboard/dashboard-server.js +329 -0
  97. package/dist/dashboard/dashboard-server.js.map +1 -0
  98. package/dist/dashboard/index.d.ts +3 -0
  99. package/dist/dashboard/index.d.ts.map +1 -0
  100. package/dist/dashboard/index.js +2 -0
  101. package/dist/dashboard/index.js.map +1 -0
  102. package/dist/dashboard/public/assets/index-Cah0_BKk.js +148 -0
  103. package/dist/dashboard/public/assets/index-CpMruPNh.css +1 -0
  104. package/dist/dashboard/public/favicon.svg +6 -0
  105. package/dist/dashboard/public/index.html +14 -0
  106. package/dist/db.d.ts +27 -0
  107. package/dist/db.d.ts.map +1 -0
  108. package/dist/db.js +209 -0
  109. package/dist/db.js.map +1 -0
  110. package/dist/doctor.d.ts +37 -0
  111. package/dist/doctor.d.ts.map +1 -0
  112. package/dist/doctor.js +216 -0
  113. package/dist/doctor.js.map +1 -0
  114. package/dist/gate/body-inspector.d.ts +31 -0
  115. package/dist/gate/body-inspector.d.ts.map +1 -0
  116. package/dist/gate/body-inspector.js +193 -0
  117. package/dist/gate/body-inspector.js.map +1 -0
  118. package/dist/gate/gate.d.ts +190 -0
  119. package/dist/gate/gate.d.ts.map +1 -0
  120. package/dist/gate/gate.js +1243 -0
  121. package/dist/gate/gate.js.map +1 -0
  122. package/dist/gate/index.d.ts +7 -0
  123. package/dist/gate/index.d.ts.map +1 -0
  124. package/dist/gate/index.js +4 -0
  125. package/dist/gate/index.js.map +1 -0
  126. package/dist/gate/rate-limiter.d.ts +59 -0
  127. package/dist/gate/rate-limiter.d.ts.map +1 -0
  128. package/dist/gate/rate-limiter.js +120 -0
  129. package/dist/gate/rate-limiter.js.map +1 -0
  130. package/dist/index.d.ts +28 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +17 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/key-storage/credential-manager-windows.d.ts +19 -0
  135. package/dist/key-storage/credential-manager-windows.d.ts.map +1 -0
  136. package/dist/key-storage/credential-manager-windows.js +87 -0
  137. package/dist/key-storage/credential-manager-windows.js.map +1 -0
  138. package/dist/key-storage/file-fallback.d.ts +21 -0
  139. package/dist/key-storage/file-fallback.d.ts.map +1 -0
  140. package/dist/key-storage/file-fallback.js +62 -0
  141. package/dist/key-storage/file-fallback.js.map +1 -0
  142. package/dist/key-storage/index.d.ts +6 -0
  143. package/dist/key-storage/index.d.ts.map +1 -0
  144. package/dist/key-storage/index.js +6 -0
  145. package/dist/key-storage/index.js.map +1 -0
  146. package/dist/key-storage/key-storage.d.ts +41 -0
  147. package/dist/key-storage/key-storage.d.ts.map +1 -0
  148. package/dist/key-storage/key-storage.js +70 -0
  149. package/dist/key-storage/key-storage.js.map +1 -0
  150. package/dist/key-storage/keychain-macos.d.ts +19 -0
  151. package/dist/key-storage/keychain-macos.d.ts.map +1 -0
  152. package/dist/key-storage/keychain-macos.js +51 -0
  153. package/dist/key-storage/keychain-macos.js.map +1 -0
  154. package/dist/key-storage/secret-service-linux.d.ts +19 -0
  155. package/dist/key-storage/secret-service-linux.d.ts.map +1 -0
  156. package/dist/key-storage/secret-service-linux.js +55 -0
  157. package/dist/key-storage/secret-service-linux.js.map +1 -0
  158. package/dist/ledger/index.d.ts +3 -0
  159. package/dist/ledger/index.d.ts.map +1 -0
  160. package/dist/ledger/index.js +2 -0
  161. package/dist/ledger/index.js.map +1 -0
  162. package/dist/ledger/ledger.d.ts +98 -0
  163. package/dist/ledger/ledger.d.ts.map +1 -0
  164. package/dist/ledger/ledger.js +145 -0
  165. package/dist/ledger/ledger.js.map +1 -0
  166. package/dist/logger/index.d.ts +3 -0
  167. package/dist/logger/index.d.ts.map +1 -0
  168. package/dist/logger/index.js +2 -0
  169. package/dist/logger/index.js.map +1 -0
  170. package/dist/logger/logger.d.ts +58 -0
  171. package/dist/logger/logger.d.ts.map +1 -0
  172. package/dist/logger/logger.js +201 -0
  173. package/dist/logger/logger.js.map +1 -0
  174. package/dist/mcp/index.d.ts +3 -0
  175. package/dist/mcp/index.d.ts.map +1 -0
  176. package/dist/mcp/index.js +2 -0
  177. package/dist/mcp/index.js.map +1 -0
  178. package/dist/mcp/mcp-server.d.ts +130 -0
  179. package/dist/mcp/mcp-server.d.ts.map +1 -0
  180. package/dist/mcp/mcp-server.js +775 -0
  181. package/dist/mcp/mcp-server.js.map +1 -0
  182. package/dist/metrics/index.d.ts +3 -0
  183. package/dist/metrics/index.d.ts.map +1 -0
  184. package/dist/metrics/index.js +2 -0
  185. package/dist/metrics/index.js.map +1 -0
  186. package/dist/metrics/metrics.d.ts +88 -0
  187. package/dist/metrics/metrics.d.ts.map +1 -0
  188. package/dist/metrics/metrics.js +179 -0
  189. package/dist/metrics/metrics.js.map +1 -0
  190. package/dist/policy/index.d.ts +3 -0
  191. package/dist/policy/index.d.ts.map +1 -0
  192. package/dist/policy/index.js +2 -0
  193. package/dist/policy/index.js.map +1 -0
  194. package/dist/policy/policy.d.ts +119 -0
  195. package/dist/policy/policy.d.ts.map +1 -0
  196. package/dist/policy/policy.js +426 -0
  197. package/dist/policy/policy.js.map +1 -0
  198. package/dist/user/index.d.ts +3 -0
  199. package/dist/user/index.d.ts.map +1 -0
  200. package/dist/user/index.js +2 -0
  201. package/dist/user/index.js.map +1 -0
  202. package/dist/user/user.d.ts +102 -0
  203. package/dist/user/user.d.ts.map +1 -0
  204. package/dist/user/user.js +216 -0
  205. package/dist/user/user.js.map +1 -0
  206. package/dist/vault/crypto.d.ts +28 -0
  207. package/dist/vault/crypto.d.ts.map +1 -0
  208. package/dist/vault/crypto.js +44 -0
  209. package/dist/vault/crypto.js.map +1 -0
  210. package/dist/vault/index.d.ts +10 -0
  211. package/dist/vault/index.d.ts.map +1 -0
  212. package/dist/vault/index.js +6 -0
  213. package/dist/vault/index.js.map +1 -0
  214. package/dist/vault/seal.d.ts +68 -0
  215. package/dist/vault/seal.d.ts.map +1 -0
  216. package/dist/vault/seal.js +110 -0
  217. package/dist/vault/seal.js.map +1 -0
  218. package/dist/vault/shamir.d.ts +33 -0
  219. package/dist/vault/shamir.d.ts.map +1 -0
  220. package/dist/vault/shamir.js +174 -0
  221. package/dist/vault/shamir.js.map +1 -0
  222. package/dist/vault/vault-manager.d.ts +62 -0
  223. package/dist/vault/vault-manager.d.ts.map +1 -0
  224. package/dist/vault/vault-manager.js +151 -0
  225. package/dist/vault/vault-manager.js.map +1 -0
  226. package/dist/vault/vault.d.ts +104 -0
  227. package/dist/vault/vault.d.ts.map +1 -0
  228. package/dist/vault/vault.js +259 -0
  229. package/dist/vault/vault.js.map +1 -0
  230. package/dist/version.d.ts +3 -0
  231. package/dist/version.d.ts.map +1 -0
  232. package/dist/version.js +18 -0
  233. package/dist/version.js.map +1 -0
  234. package/dist/webhook/index.d.ts +3 -0
  235. package/dist/webhook/index.d.ts.map +1 -0
  236. package/dist/webhook/index.js +2 -0
  237. package/dist/webhook/index.js.map +1 -0
  238. package/dist/webhook/webhook.d.ts +114 -0
  239. package/dist/webhook/webhook.d.ts.map +1 -0
  240. package/dist/webhook/webhook.js +269 -0
  241. package/dist/webhook/webhook.js.map +1 -0
  242. package/package.json +12 -6
@@ -0,0 +1,775 @@
1
+ /**
2
+ * Aegis MCP Server — exposes Aegis credential isolation as MCP tools.
3
+ *
4
+ * The MCP server sits between an AI agent (MCP client) and external APIs.
5
+ * The agent uses tools to make authenticated API calls without ever seeing credentials.
6
+ *
7
+ * Tools:
8
+ * - aegis_proxy_request: Make an authenticated API call through Aegis
9
+ * - aegis_list_services: List available services (names only, never secrets)
10
+ * - aegis_health: Check Aegis status
11
+ *
12
+ * Transports:
13
+ * - stdio: For local process-spawned integrations (Claude Desktop, Cursor, VS Code)
14
+ * - streamable-http: For remote server access
15
+ */
16
+ import * as http from 'node:http';
17
+ import * as https from 'node:https';
18
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
21
+ import { z } from 'zod';
22
+ import { BodyInspector } from '../gate/body-inspector.js';
23
+ import { methodMatchesScope } from '../gate/gate.js';
24
+ import { parseRateLimit, RateLimiter } from '../gate/rate-limiter.js';
25
+ import { createLogger, generateRequestId } from '../logger/index.js';
26
+ import { buildPolicyMap, evaluatePolicy } from '../policy/index.js';
27
+ import { VERSION } from '../version.js';
28
+ // ─── AegisMcpServer ─────────────────────────────────────────────
29
+ /**
30
+ * Aegis MCP Server — wraps the Aegis credential isolation layer as an MCP server.
31
+ *
32
+ * This gives any MCP-compatible AI agent (Claude, ChatGPT, Cursor, VS Code Copilot)
33
+ * the ability to make authenticated API calls without ever seeing credentials.
34
+ */
35
+ export class AegisMcpServer {
36
+ server;
37
+ vault;
38
+ ledger;
39
+ agentRegistry;
40
+ authenticatedAgent;
41
+ transportType;
42
+ port;
43
+ policyMap;
44
+ policyMode;
45
+ logger;
46
+ rateLimiter;
47
+ bodyInspector;
48
+ httpServer;
49
+ metrics;
50
+ webhooks;
51
+ constructor(options) {
52
+ this.vault = options.vault;
53
+ this.ledger = options.ledger;
54
+ this.agentRegistry = options.agentRegistry;
55
+ this.transportType = options.transport;
56
+ this.port = options.port ?? 3200;
57
+ this.policyMode = options.policyMode ?? 'enforce';
58
+ this.logger = createLogger({
59
+ module: 'mcp',
60
+ level: options.logLevel ?? 'info',
61
+ // stdio transport: logs must go to stderr (stdout is reserved for MCP protocol messages)
62
+ stderr: options.transport === 'stdio',
63
+ });
64
+ this.rateLimiter = new RateLimiter();
65
+ this.bodyInspector = new BodyInspector();
66
+ this.metrics = options.metrics;
67
+ this.webhooks = options.webhooks;
68
+ // Build policy map from provided policies
69
+ if (options.policies && options.policies.length > 0) {
70
+ this.policyMap = buildPolicyMap(options.policies);
71
+ }
72
+ else {
73
+ this.policyMap = new Map();
74
+ }
75
+ // Authenticate the agent if a token was provided
76
+ if (options.agentToken && options.agentRegistry) {
77
+ const agent = options.agentRegistry.validateToken(options.agentToken);
78
+ if (!agent) {
79
+ throw new Error('Invalid agent token provided for MCP server. Check your token or register a new agent with: aegis agent add');
80
+ }
81
+ this.authenticatedAgent = agent;
82
+ }
83
+ // Create the MCP server
84
+ this.server = new McpServer({
85
+ name: 'aegis',
86
+ version: VERSION,
87
+ });
88
+ // Register tools
89
+ this.registerTools();
90
+ }
91
+ // ─── Tool Registration ─────────────────────────────────────────
92
+ registerTools() {
93
+ this.registerProxyRequestTool();
94
+ this.registerListServicesTool();
95
+ this.registerHealthTool();
96
+ }
97
+ /**
98
+ * aegis_proxy_request — Make an authenticated API call through Aegis.
99
+ *
100
+ * The agent provides service, path, method, headers, and body.
101
+ * Aegis injects credentials, enforces domain guard, rate limits, body inspection,
102
+ * and policy evaluation — then returns the response.
103
+ */
104
+ registerProxyRequestTool() {
105
+ this.server.registerTool('aegis_proxy_request', {
106
+ title: 'Aegis Proxy Request',
107
+ description: 'Make an authenticated API call through Aegis. Credentials are injected automatically — you never see them. Provide the service name and API path; Aegis handles authentication.',
108
+ inputSchema: {
109
+ service: z
110
+ .string()
111
+ .describe('The service name (must match a registered credential in Aegis)'),
112
+ path: z.string().describe('The API path to call (e.g. "/v1/chat/completions")'),
113
+ method: z
114
+ .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])
115
+ .optional()
116
+ .default('GET')
117
+ .describe('HTTP method (default: GET)'),
118
+ headers: z
119
+ .record(z.string(), z.string())
120
+ .optional()
121
+ .describe('Additional request headers (auth headers are injected automatically)'),
122
+ body: z.string().optional().describe('Request body (for POST/PUT/PATCH)'),
123
+ targetHost: z
124
+ .string()
125
+ .optional()
126
+ .describe("Override the target domain (must be in the credential's allowlist). Defaults to the credential's primary domain."),
127
+ },
128
+ }, async (args) => {
129
+ try {
130
+ const result = await this.proxyRequest(args);
131
+ return {
132
+ content: [
133
+ {
134
+ type: 'text',
135
+ text: JSON.stringify({
136
+ status: result.status,
137
+ headers: result.headers,
138
+ body: result.body,
139
+ }, null, 2),
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ catch (err) {
145
+ const message = err instanceof Error ? err.message : String(err);
146
+ return {
147
+ content: [{ type: 'text', text: `Error: ${message}` }],
148
+ isError: true,
149
+ };
150
+ }
151
+ });
152
+ }
153
+ /**
154
+ * aegis_list_services — List available services the agent can use.
155
+ *
156
+ * Returns service names and domains only — never secrets.
157
+ */
158
+ registerListServicesTool() {
159
+ this.server.registerTool('aegis_list_services', {
160
+ title: 'Aegis List Services',
161
+ description: 'List all available services registered in Aegis. Returns service names, auth types, and allowed domains — never secrets.',
162
+ inputSchema: {},
163
+ }, async () => {
164
+ const credentials = this.vault.list();
165
+ // If an agent is authenticated, filter to only their granted credentials
166
+ let filtered = credentials;
167
+ if (this.authenticatedAgent && this.agentRegistry) {
168
+ const grantedIds = this.agentRegistry.listGrants(this.authenticatedAgent.name);
169
+ if (grantedIds.length > 0) {
170
+ filtered = credentials.filter((c) => grantedIds.includes(c.id));
171
+ }
172
+ }
173
+ const services = filtered.map((c) => ({
174
+ name: c.name,
175
+ service: c.service,
176
+ authType: c.authType,
177
+ domains: c.domains,
178
+ scopes: c.scopes,
179
+ expiresAt: c.expiresAt ?? null,
180
+ rateLimit: c.rateLimit ?? null,
181
+ }));
182
+ return {
183
+ content: [
184
+ {
185
+ type: 'text',
186
+ text: JSON.stringify({ services, total: services.length }, null, 2),
187
+ },
188
+ ],
189
+ };
190
+ });
191
+ }
192
+ /**
193
+ * aegis_health — Check Aegis status.
194
+ */
195
+ registerHealthTool() {
196
+ this.server.registerTool('aegis_health', {
197
+ title: 'Aegis Health Check',
198
+ description: 'Check the health status of Aegis, including credential and agent counts.',
199
+ inputSchema: {},
200
+ }, async () => {
201
+ const credentials = this.vault.list();
202
+ const stats = this.ledger.stats();
203
+ const agents = this.agentRegistry?.list() ?? [];
204
+ const health = {
205
+ status: 'ok',
206
+ version: VERSION,
207
+ credentials: {
208
+ total: credentials.length,
209
+ expired: credentials.filter((c) => c.expiresAt && new Date(c.expiresAt) < new Date())
210
+ .length,
211
+ active: credentials.filter((c) => !c.expiresAt || new Date(c.expiresAt) >= new Date())
212
+ .length,
213
+ },
214
+ agents: {
215
+ total: agents.length,
216
+ },
217
+ audit: stats,
218
+ authenticatedAgent: this.authenticatedAgent
219
+ ? {
220
+ name: this.authenticatedAgent.name,
221
+ tokenPrefix: this.authenticatedAgent.tokenPrefix,
222
+ }
223
+ : null,
224
+ };
225
+ return {
226
+ content: [{ type: 'text', text: JSON.stringify(health, null, 2) }],
227
+ };
228
+ });
229
+ }
230
+ // ─── Proxy Logic ───────────────────────────────────────────────
231
+ /**
232
+ * Execute an authenticated proxy request through Aegis.
233
+ *
234
+ * This replicates the Gate's security pipeline:
235
+ * 1. Credential lookup
236
+ * 2. TTL check
237
+ * 3. Agent credential scoping
238
+ * 4. Policy evaluation
239
+ * 5. Agent rate limiting
240
+ * 6. Credential rate limiting
241
+ * 7. Domain guard
242
+ * 8. Body inspection
243
+ * 9. Credential injection + forward
244
+ * 10. Audit logging
245
+ */
246
+ async proxyRequest(params) {
247
+ const method = params.method ?? 'GET';
248
+ const path = params.path.startsWith('/') ? params.path : `/${params.path}`;
249
+ const requestId = generateRequestId();
250
+ // 1. Credential lookup
251
+ const credential = this.vault.getByService(params.service);
252
+ if (!credential) {
253
+ this.metrics?.recordBlocked(params.service, 'no_credential', this.authenticatedAgent?.name);
254
+ this.webhooks?.emit('blocked_request', {
255
+ service: params.service,
256
+ reason: 'no_credential',
257
+ method,
258
+ path,
259
+ agent: this.authenticatedAgent?.name,
260
+ });
261
+ this.ledger.logBlocked({
262
+ service: params.service,
263
+ targetDomain: 'unknown',
264
+ method,
265
+ path,
266
+ reason: `No credential found for service: ${params.service}`,
267
+ agentName: this.authenticatedAgent?.name,
268
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
269
+ channel: 'mcp',
270
+ });
271
+ throw new Error(`No credential registered for service: ${params.service}. ` +
272
+ `Register one with: aegis vault add --name ${params.service} --service ${params.service} --secret YOUR_KEY --domains api.example.com`);
273
+ }
274
+ // 2. TTL enforcement
275
+ if (this.vault.isExpired(credential)) {
276
+ this.metrics?.recordBlocked(params.service, 'credential_expired', this.authenticatedAgent?.name);
277
+ this.webhooks?.emit('credential_expiry', {
278
+ service: params.service,
279
+ credential: credential.name,
280
+ expiredAt: credential.expiresAt,
281
+ agent: this.authenticatedAgent?.name,
282
+ });
283
+ this.ledger.logBlocked({
284
+ service: params.service,
285
+ targetDomain: credential.domains[0] ?? 'unknown',
286
+ method,
287
+ path,
288
+ reason: `Credential "${credential.name}" expired at ${credential.expiresAt}`,
289
+ agentName: this.authenticatedAgent?.name,
290
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
291
+ channel: 'mcp',
292
+ });
293
+ throw new Error(`Credential "${credential.name}" has expired at ${credential.expiresAt}. ` +
294
+ `Rotate with: aegis vault rotate --name ${credential.name} --secret NEW_SECRET`);
295
+ }
296
+ // 3. Agent credential scoping
297
+ if (this.authenticatedAgent && this.agentRegistry) {
298
+ if (!this.agentRegistry.hasAccess(this.authenticatedAgent.id, credential.id)) {
299
+ this.metrics?.recordBlocked(params.service, 'agent_scope', this.authenticatedAgent.name);
300
+ this.webhooks?.emit('blocked_request', {
301
+ service: params.service,
302
+ reason: 'agent_scope',
303
+ agent: this.authenticatedAgent.name,
304
+ credential: credential.name,
305
+ method,
306
+ path,
307
+ });
308
+ this.ledger.logBlocked({
309
+ service: params.service,
310
+ targetDomain: credential.domains[0] ?? 'unknown',
311
+ method,
312
+ path,
313
+ reason: `Agent "${this.authenticatedAgent.name}" not granted access to credential "${credential.name}"`,
314
+ agentName: this.authenticatedAgent.name,
315
+ agentTokenPrefix: this.authenticatedAgent.tokenPrefix,
316
+ channel: 'mcp',
317
+ });
318
+ throw new Error(`Agent "${this.authenticatedAgent.name}" is not granted access to credential "${credential.name}". ` +
319
+ `Grant access with: aegis agent grant --agent ${this.authenticatedAgent.name} --credential ${credential.name}`);
320
+ }
321
+ }
322
+ // 4. Credential scope enforcement
323
+ if (!methodMatchesScope(method, credential.scopes)) {
324
+ const scopeList = credential.scopes.join(', ');
325
+ this.metrics?.recordBlocked(params.service, 'credential_scope', this.authenticatedAgent?.name);
326
+ this.webhooks?.emit('blocked_request', {
327
+ service: params.service,
328
+ reason: 'credential_scope',
329
+ credential: credential.name,
330
+ method,
331
+ scopes: credential.scopes,
332
+ agent: this.authenticatedAgent?.name,
333
+ path,
334
+ });
335
+ this.ledger.logBlocked({
336
+ service: params.service,
337
+ targetDomain: credential.domains[0] ?? 'unknown',
338
+ method,
339
+ path,
340
+ reason: `Method "${method}" not permitted by credential scopes [${scopeList}]`,
341
+ agentName: this.authenticatedAgent?.name,
342
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
343
+ channel: 'mcp',
344
+ });
345
+ throw new Error(`Method "${method}" is not permitted by credential scopes [${scopeList}]. ` +
346
+ `Update scopes with: aegis vault update --name ${credential.name} --scopes ${scopeList},${method === 'GET' ? 'read' : 'write'}`);
347
+ }
348
+ // 5. Policy evaluation
349
+ if (this.authenticatedAgent && this.policyMap.size > 0) {
350
+ const agentPolicy = this.policyMap.get(this.authenticatedAgent.name);
351
+ if (agentPolicy) {
352
+ const evaluation = evaluatePolicy(agentPolicy, {
353
+ service: params.service,
354
+ method,
355
+ path,
356
+ });
357
+ if (!evaluation.allowed) {
358
+ const reason = `Policy violation: ${evaluation.reason}`;
359
+ if (this.policyMode === 'enforce') {
360
+ this.metrics?.recordBlocked(params.service, 'policy_violation', this.authenticatedAgent.name);
361
+ this.webhooks?.emit('blocked_request', {
362
+ service: params.service,
363
+ reason: 'policy_violation',
364
+ agent: this.authenticatedAgent.name,
365
+ violation: evaluation.violation,
366
+ detail: evaluation.reason,
367
+ method,
368
+ path,
369
+ });
370
+ this.ledger.logBlocked({
371
+ service: params.service,
372
+ targetDomain: credential.domains[0] ?? 'unknown',
373
+ method,
374
+ path,
375
+ reason,
376
+ agentName: this.authenticatedAgent.name,
377
+ agentTokenPrefix: this.authenticatedAgent.tokenPrefix,
378
+ channel: 'mcp',
379
+ });
380
+ throw new Error(`Policy violation for agent "${this.authenticatedAgent.name}": ${evaluation.reason}`);
381
+ }
382
+ // Dry-run: log but allow
383
+ this.ledger.logBlocked({
384
+ service: params.service,
385
+ targetDomain: credential.domains[0] ?? 'unknown',
386
+ method,
387
+ path,
388
+ reason: `POLICY_DRY_RUN: ${evaluation.reason}`,
389
+ agentName: this.authenticatedAgent.name,
390
+ agentTokenPrefix: this.authenticatedAgent.tokenPrefix,
391
+ channel: 'mcp',
392
+ });
393
+ this.logger.info({ agent: this.authenticatedAgent.name, reason: evaluation.reason, requestId }, 'Dry-run: would block');
394
+ }
395
+ }
396
+ }
397
+ // 6. Agent rate limiting
398
+ if (this.authenticatedAgent?.rateLimit) {
399
+ try {
400
+ const agentParsedLimit = parseRateLimit(this.authenticatedAgent.rateLimit);
401
+ const agentResult = this.rateLimiter.check(`agent:${this.authenticatedAgent.id}`, agentParsedLimit);
402
+ if (!agentResult.allowed) {
403
+ this.metrics?.recordBlocked(params.service, 'agent_rate_limit', this.authenticatedAgent.name);
404
+ this.webhooks?.emit('rate_limit_exceeded', {
405
+ service: params.service,
406
+ type: 'agent',
407
+ agent: this.authenticatedAgent.name,
408
+ limit: this.authenticatedAgent.rateLimit,
409
+ retryAfter: agentResult.retryAfterSeconds,
410
+ });
411
+ this.ledger.logBlocked({
412
+ service: params.service,
413
+ targetDomain: credential.domains[0] ?? 'unknown',
414
+ method,
415
+ path,
416
+ reason: `Agent rate limit exceeded: ${this.authenticatedAgent.rateLimit} (retry after ${agentResult.retryAfterSeconds}s)`,
417
+ agentName: this.authenticatedAgent.name,
418
+ agentTokenPrefix: this.authenticatedAgent.tokenPrefix,
419
+ channel: 'mcp',
420
+ });
421
+ throw new Error(`Agent rate limit exceeded (${this.authenticatedAgent.rateLimit}). Retry after ${agentResult.retryAfterSeconds} seconds.`);
422
+ }
423
+ }
424
+ catch (err) {
425
+ // Re-throw rate limit errors, ignore parse errors
426
+ if (err instanceof Error && err.message.includes('rate limit')) {
427
+ throw err;
428
+ }
429
+ this.logger.error({
430
+ agent: this.authenticatedAgent.name,
431
+ rateLimit: this.authenticatedAgent.rateLimit,
432
+ requestId,
433
+ }, 'Invalid agent rate limit config');
434
+ }
435
+ }
436
+ // 7. Credential rate limiting
437
+ if (credential.rateLimit) {
438
+ try {
439
+ const parsedLimit = parseRateLimit(credential.rateLimit);
440
+ const result = this.rateLimiter.check(credential.id, parsedLimit);
441
+ if (!result.allowed) {
442
+ this.metrics?.recordBlocked(params.service, 'credential_rate_limit', this.authenticatedAgent?.name);
443
+ this.webhooks?.emit('rate_limit_exceeded', {
444
+ service: params.service,
445
+ type: 'credential',
446
+ credential: credential.name,
447
+ limit: credential.rateLimit,
448
+ retryAfter: result.retryAfterSeconds,
449
+ agent: this.authenticatedAgent?.name,
450
+ });
451
+ this.ledger.logBlocked({
452
+ service: params.service,
453
+ targetDomain: credential.domains[0] ?? 'unknown',
454
+ method,
455
+ path,
456
+ reason: `Rate limit exceeded: ${credential.rateLimit} (retry after ${result.retryAfterSeconds}s)`,
457
+ agentName: this.authenticatedAgent?.name,
458
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
459
+ channel: 'mcp',
460
+ });
461
+ throw new Error(`Rate limit exceeded for "${credential.name}" (${credential.rateLimit}). Retry after ${result.retryAfterSeconds} seconds.`);
462
+ }
463
+ }
464
+ catch (err) {
465
+ if (err instanceof Error && err.message.includes('Rate limit')) {
466
+ throw err;
467
+ }
468
+ this.logger.error({ credential: credential.name, rateLimit: credential.rateLimit, requestId }, 'Invalid credential rate limit config');
469
+ }
470
+ }
471
+ // 8. Domain guard
472
+ const targetDomain = params.targetHost ?? credential.domains[0];
473
+ if (!this.vault.domainMatches(targetDomain, credential.domains)) {
474
+ this.metrics?.recordBlocked(params.service, 'domain_guard', this.authenticatedAgent?.name);
475
+ this.webhooks?.emit('blocked_request', {
476
+ service: params.service,
477
+ reason: 'domain_guard',
478
+ targetDomain,
479
+ allowedDomains: credential.domains,
480
+ agent: this.authenticatedAgent?.name,
481
+ method,
482
+ path,
483
+ });
484
+ this.ledger.logBlocked({
485
+ service: params.service,
486
+ targetDomain,
487
+ method,
488
+ path,
489
+ reason: `Domain "${targetDomain}" not in allowlist [${credential.domains.join(', ')}]`,
490
+ agentName: this.authenticatedAgent?.name,
491
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
492
+ channel: 'mcp',
493
+ });
494
+ throw new Error(`Domain "${targetDomain}" is not in the credential's allowlist [${credential.domains.join(', ')}].`);
495
+ }
496
+ // 9. Body inspection
497
+ if (credential.bodyInspection !== 'off' && params.body && params.body.length > 0) {
498
+ const inspection = this.bodyInspector.inspect(params.body);
499
+ if (inspection.suspicious) {
500
+ const matchSummary = inspection.matches.join('; ');
501
+ if (credential.bodyInspection === 'block') {
502
+ this.metrics?.recordBlocked(params.service, 'body_inspection', this.authenticatedAgent?.name);
503
+ this.webhooks?.emit('body_inspection', {
504
+ service: params.service,
505
+ credential: credential.name,
506
+ matches: inspection.matches,
507
+ agent: this.authenticatedAgent?.name,
508
+ method,
509
+ path,
510
+ });
511
+ this.ledger.logBlocked({
512
+ service: params.service,
513
+ targetDomain,
514
+ method,
515
+ path,
516
+ reason: `Body inspection: potential credential exfiltration — ${matchSummary}`,
517
+ agentName: this.authenticatedAgent?.name,
518
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
519
+ channel: 'mcp',
520
+ });
521
+ throw new Error(`Request body contains credential-like patterns: ${matchSummary}. ` +
522
+ "If this is intentional, set body inspection to 'warn' or 'off' for this credential.");
523
+ }
524
+ // Warn mode
525
+ this.logger.warn({ credential: credential.name, matches: inspection.matches, requestId }, 'Body inspection: credential-like patterns detected (warn mode)');
526
+ }
527
+ }
528
+ // 10. Make the outbound request with credential injection
529
+ const result = await this.makeOutboundRequest(credential, {
530
+ targetDomain,
531
+ path,
532
+ method,
533
+ headers: params.headers,
534
+ body: params.body,
535
+ });
536
+ // 11. Audit log
537
+ this.ledger.logAllowed({
538
+ credentialId: credential.id,
539
+ credentialName: credential.name,
540
+ service: params.service,
541
+ targetDomain,
542
+ method,
543
+ path,
544
+ responseCode: result.status,
545
+ agentName: this.authenticatedAgent?.name,
546
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
547
+ channel: 'mcp',
548
+ });
549
+ this.logger.info({ service: params.service, method, path, status: result.status, requestId }, 'MCP request proxied');
550
+ this.metrics?.recordRequest(params.service, method, result.status, this.authenticatedAgent?.name);
551
+ return result;
552
+ }
553
+ /**
554
+ * Make the actual outbound HTTP request with credential injection.
555
+ */
556
+ makeOutboundRequest(credential, params) {
557
+ return new Promise((resolve, reject) => {
558
+ const outboundHeaders = {
559
+ host: params.targetDomain,
560
+ };
561
+ // Add user-provided headers (but strip any auth headers the agent tried to add)
562
+ if (params.headers) {
563
+ for (const [key, value] of Object.entries(params.headers)) {
564
+ const lower = key.toLowerCase();
565
+ if (lower === 'authorization' ||
566
+ lower === 'x-api-key' ||
567
+ lower === 'x-aegis-agent' ||
568
+ lower === 'x-target-host') {
569
+ continue; // Strip auth headers — Aegis injects the real ones
570
+ }
571
+ outboundHeaders[key] = value;
572
+ }
573
+ }
574
+ // Set content-type for bodies if not already set
575
+ if (params.body && !outboundHeaders['content-type'] && !outboundHeaders['Content-Type']) {
576
+ outboundHeaders['content-type'] = 'application/json';
577
+ }
578
+ // Inject the real credential (query auth may modify the path)
579
+ const injectedPath = this.injectCredential(outboundHeaders, credential, params.path);
580
+ const proxyReq = https.request({
581
+ hostname: params.targetDomain,
582
+ port: 443,
583
+ path: injectedPath ?? params.path,
584
+ method: params.method,
585
+ headers: outboundHeaders,
586
+ }, (proxyRes) => {
587
+ const chunks = [];
588
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
589
+ proxyRes.on('end', () => {
590
+ const body = Buffer.concat(chunks).toString('utf-8');
591
+ // Build clean response headers (strip set-cookie for security)
592
+ const responseHeaders = {};
593
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
594
+ if (key.toLowerCase() === 'set-cookie')
595
+ continue;
596
+ if (value !== undefined) {
597
+ responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
598
+ }
599
+ }
600
+ resolve({
601
+ status: proxyRes.statusCode ?? 500,
602
+ headers: responseHeaders,
603
+ body,
604
+ });
605
+ });
606
+ proxyRes.on('error', (err) => reject(new Error(`Response error: ${err.message}`)));
607
+ });
608
+ proxyReq.on('error', (err) => {
609
+ this.ledger.logBlocked({
610
+ service: credential.service,
611
+ targetDomain: params.targetDomain,
612
+ method: params.method,
613
+ path: params.path,
614
+ reason: `Proxy error: ${err.message}`,
615
+ agentName: this.authenticatedAgent?.name,
616
+ agentTokenPrefix: this.authenticatedAgent?.tokenPrefix,
617
+ channel: 'mcp',
618
+ });
619
+ reject(new Error(`Failed to reach upstream service: ${err.message}`));
620
+ });
621
+ if (params.body) {
622
+ proxyReq.write(params.body);
623
+ }
624
+ proxyReq.end();
625
+ });
626
+ }
627
+ /**
628
+ * Inject the credential into outbound request headers based on auth type.
629
+ * For `query` auth, the secret is appended as a URL query parameter instead.
630
+ */
631
+ injectCredential(headers, credential, path) {
632
+ switch (credential.authType) {
633
+ case 'bearer':
634
+ headers.authorization = `Bearer ${credential.secret}`;
635
+ break;
636
+ case 'header':
637
+ headers[credential.headerName ?? 'x-api-key'] = credential.secret;
638
+ break;
639
+ case 'basic':
640
+ headers.authorization = `Basic ${Buffer.from(credential.secret).toString('base64')}`;
641
+ break;
642
+ case 'query': {
643
+ if (path !== undefined) {
644
+ const paramName = encodeURIComponent(credential.headerName ?? 'key');
645
+ const paramValue = encodeURIComponent(credential.secret);
646
+ const separator = path.includes('?') ? '&' : '?';
647
+ return `${path}${separator}${paramName}=${paramValue}`;
648
+ }
649
+ break;
650
+ }
651
+ }
652
+ return path;
653
+ }
654
+ // ─── Transport & Lifecycle ─────────────────────────────────────
655
+ /**
656
+ * Start the MCP server with the configured transport.
657
+ */
658
+ async start() {
659
+ if (this.transportType === 'stdio') {
660
+ await this.startStdio();
661
+ }
662
+ else {
663
+ await this.startStreamableHttp();
664
+ }
665
+ }
666
+ /**
667
+ * Start with stdio transport (for local integrations).
668
+ */
669
+ async startStdio() {
670
+ const transport = new StdioServerTransport();
671
+ await this.server.connect(transport);
672
+ this.logger.info({ transport: 'stdio' }, 'Aegis MCP server started');
673
+ }
674
+ /**
675
+ * Start with Streamable HTTP transport (for remote access).
676
+ */
677
+ async startStreamableHttp() {
678
+ // Track active transports for stateful sessions
679
+ const transports = new Map();
680
+ this.httpServer = http.createServer(async (req, res) => {
681
+ const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
682
+ // Only handle /mcp endpoint
683
+ if (url.pathname !== '/mcp') {
684
+ res.writeHead(404, { 'Content-Type': 'application/json' });
685
+ res.end(JSON.stringify({ error: 'Not found. MCP endpoint is at /mcp' }));
686
+ return;
687
+ }
688
+ // Handle session management
689
+ const sessionId = req.headers['mcp-session-id'];
690
+ if (req.method === 'POST') {
691
+ // Check for existing session
692
+ if (sessionId && transports.has(sessionId)) {
693
+ const transport = transports.get(sessionId);
694
+ if (transport) {
695
+ await transport.handleRequest(req, res);
696
+ return;
697
+ }
698
+ }
699
+ // New session — create a new transport
700
+ const transport = new StreamableHTTPServerTransport({
701
+ sessionIdGenerator: () => crypto.randomUUID(),
702
+ onsessioninitialized: (newSessionId) => {
703
+ transports.set(newSessionId, transport);
704
+ this.logger.debug({ sessionId: newSessionId }, 'MCP session started');
705
+ },
706
+ });
707
+ // Clean up on session close
708
+ transport.onclose = () => {
709
+ const sid = Array.from(transports.entries()).find(([, t]) => t === transport)?.[0];
710
+ if (sid) {
711
+ transports.delete(sid);
712
+ this.logger.debug({ sessionId: sid }, 'MCP session closed');
713
+ }
714
+ };
715
+ await this.server.connect(transport);
716
+ await transport.handleRequest(req, res);
717
+ }
718
+ else if (req.method === 'GET') {
719
+ // SSE stream for server-to-client notifications
720
+ if (sessionId && transports.has(sessionId)) {
721
+ const transport = transports.get(sessionId);
722
+ if (transport) {
723
+ await transport.handleRequest(req, res);
724
+ return;
725
+ }
726
+ }
727
+ res.writeHead(400, { 'Content-Type': 'application/json' });
728
+ res.end(JSON.stringify({ error: 'Missing or invalid session ID' }));
729
+ }
730
+ else if (req.method === 'DELETE') {
731
+ // Session termination
732
+ if (sessionId && transports.has(sessionId)) {
733
+ const transport = transports.get(sessionId);
734
+ if (transport) {
735
+ await transport.handleRequest(req, res);
736
+ transports.delete(sessionId);
737
+ this.logger.debug({ sessionId }, 'MCP session terminated');
738
+ return;
739
+ }
740
+ }
741
+ res.writeHead(404, { 'Content-Type': 'application/json' });
742
+ res.end(JSON.stringify({ error: 'Session not found' }));
743
+ }
744
+ else {
745
+ res.writeHead(405, { 'Content-Type': 'application/json' });
746
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
747
+ }
748
+ });
749
+ await new Promise((resolve) => {
750
+ this.httpServer?.listen(this.port, '127.0.0.1', () => {
751
+ this.logger.info({ transport: 'streamable-http', host: '127.0.0.1', port: this.port, endpoint: '/mcp' }, 'Aegis MCP server started');
752
+ resolve();
753
+ });
754
+ });
755
+ }
756
+ /**
757
+ * Stop the MCP server.
758
+ */
759
+ async stop() {
760
+ if (this.httpServer) {
761
+ await new Promise((resolve, reject) => {
762
+ this.httpServer?.close((err) => {
763
+ if (err)
764
+ reject(err);
765
+ else
766
+ resolve();
767
+ });
768
+ });
769
+ this.httpServer = undefined;
770
+ }
771
+ await this.server.close();
772
+ this.logger.info('Aegis MCP server stopped');
773
+ }
774
+ }
775
+ //# sourceMappingURL=mcp-server.js.map