@getaegis/cli 0.8.0 → 0.8.1
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 +5 -0
- package/dist/agent/agent.d.ts +98 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +212 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/cli/auth.d.ts +19 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +44 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +6 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +241 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/config.d.ts +6 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +125 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +6 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +189 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +39 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/gate.d.ts +6 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +196 -0
- package/dist/cli/commands/gate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +109 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/ledger.d.ts +6 -0
- package/dist/cli/commands/ledger.d.ts.map +1 -0
- package/dist/cli/commands/ledger.js +140 -0
- package/dist/cli/commands/ledger.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +224 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/policy.d.ts +6 -0
- package/dist/cli/commands/policy.d.ts.map +1 -0
- package/dist/cli/commands/policy.js +126 -0
- package/dist/cli/commands/policy.js.map +1 -0
- package/dist/cli/commands/user.d.ts +6 -0
- package/dist/cli/commands/user.d.ts.map +1 -0
- package/dist/cli/commands/user.js +150 -0
- package/dist/cli/commands/user.js.map +1 -0
- package/dist/cli/commands/vault-manager.d.ts +6 -0
- package/dist/cli/commands/vault-manager.d.ts.map +1 -0
- package/dist/cli/commands/vault-manager.js +240 -0
- package/dist/cli/commands/vault-manager.js.map +1 -0
- package/dist/cli/commands/vault.d.ts +6 -0
- package/dist/cli/commands/vault.d.ts.map +1 -0
- package/dist/cli/commands/vault.js +241 -0
- package/dist/cli/commands/vault.js.map +1 -0
- package/dist/cli/commands/webhook.d.ts +6 -0
- package/dist/cli/commands/webhook.d.ts.map +1 -0
- package/dist/cli/commands/webhook.js +151 -0
- package/dist/cli/commands/webhook.js.map +1 -0
- package/dist/cli/helpers.d.ts +12 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/helpers.js +61 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/validation.d.ts +37 -0
- package/dist/cli/validation.d.ts.map +1 -0
- package/dist/cli/validation.js +104 -0
- package/dist/cli/validation.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +30 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +355 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/dashboard-server.d.ts +95 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard/dashboard-server.js +329 -0
- package/dist/dashboard/dashboard-server.js.map +1 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/public/assets/index-CpMruPNh.css +1 -0
- package/dist/dashboard/public/assets/index-DkHiw9_f.js +148 -0
- package/dist/dashboard/public/favicon.svg +6 -0
- package/dist/dashboard/public/index.html +14 -0
- package/dist/db.d.ts +15 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +190 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +37 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +196 -0
- package/dist/doctor.js.map +1 -0
- package/dist/gate/body-inspector.d.ts +31 -0
- package/dist/gate/body-inspector.d.ts.map +1 -0
- package/dist/gate/body-inspector.js +193 -0
- package/dist/gate/body-inspector.js.map +1 -0
- package/dist/gate/gate.d.ts +168 -0
- package/dist/gate/gate.d.ts.map +1 -0
- package/dist/gate/gate.js +1016 -0
- package/dist/gate/gate.js.map +1 -0
- package/dist/gate/index.d.ts +7 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +4 -0
- package/dist/gate/index.js.map +1 -0
- package/dist/gate/rate-limiter.d.ts +59 -0
- package/dist/gate/rate-limiter.d.ts.map +1 -0
- package/dist/gate/rate-limiter.js +120 -0
- package/dist/gate/rate-limiter.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger/index.d.ts +3 -0
- package/dist/ledger/index.d.ts.map +1 -0
- package/dist/ledger/index.js +2 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/ledger.d.ts +98 -0
- package/dist/ledger/ledger.d.ts.map +1 -0
- package/dist/ledger/ledger.js +145 -0
- package/dist/ledger/ledger.js.map +1 -0
- package/dist/logger/index.d.ts +3 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +58 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +201 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +130 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +775 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics.d.ts +88 -0
- package/dist/metrics/metrics.d.ts.map +1 -0
- package/dist/metrics/metrics.js +179 -0
- package/dist/metrics/metrics.js.map +1 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/policy.d.ts +119 -0
- package/dist/policy/policy.d.ts.map +1 -0
- package/dist/policy/policy.js +426 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/user/index.d.ts +3 -0
- package/dist/user/index.d.ts.map +1 -0
- package/dist/user/index.js +2 -0
- package/dist/user/index.js.map +1 -0
- package/dist/user/user.d.ts +102 -0
- package/dist/user/user.d.ts.map +1 -0
- package/dist/user/user.js +216 -0
- package/dist/user/user.js.map +1 -0
- package/dist/vault/crypto.d.ts +28 -0
- package/dist/vault/crypto.d.ts.map +1 -0
- package/dist/vault/crypto.js +44 -0
- package/dist/vault/crypto.js.map +1 -0
- package/dist/vault/index.d.ts +10 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +6 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/seal.d.ts +68 -0
- package/dist/vault/seal.d.ts.map +1 -0
- package/dist/vault/seal.js +110 -0
- package/dist/vault/seal.js.map +1 -0
- package/dist/vault/shamir.d.ts +33 -0
- package/dist/vault/shamir.d.ts.map +1 -0
- package/dist/vault/shamir.js +174 -0
- package/dist/vault/shamir.js.map +1 -0
- package/dist/vault/vault-manager.d.ts +62 -0
- package/dist/vault/vault-manager.d.ts.map +1 -0
- package/dist/vault/vault-manager.js +141 -0
- package/dist/vault/vault-manager.js.map +1 -0
- package/dist/vault/vault.d.ts +104 -0
- package/dist/vault/vault.d.ts.map +1 -0
- package/dist/vault/vault.js +259 -0
- package/dist/vault/vault.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/dist/webhook/index.d.ts +3 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/dist/webhook/index.js +2 -0
- package/dist/webhook/index.js.map +1 -0
- package/dist/webhook/webhook.d.ts +114 -0
- package/dist/webhook/webhook.d.ts.map +1 -0
- package/dist/webhook/webhook.js +269 -0
- package/dist/webhook/webhook.js.map +1 -0
- package/package.json +7 -3
|
@@ -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
|