palaryn 0.1.0 → 0.3.2
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 +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Gateway } from '../server/gateway';
|
|
7
|
+
import { ToolCall, ToolCallArgs, ToolInfo } from '../types/tool-call';
|
|
8
|
+
import { ToolResult } from '../types/tool-result';
|
|
9
|
+
import { internalAuthTokens } from './internal-auth';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Configuration
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface MCPHttpConfig {
|
|
16
|
+
/** Platform identifier (default: 'mcp_http') */
|
|
17
|
+
platform?: string;
|
|
18
|
+
/** Gateway's own base URL — when set, self-referencing requests get the OAuth token injected */
|
|
19
|
+
gateway_base_url?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ResolvedMCPHttpConfig {
|
|
23
|
+
platform: string;
|
|
24
|
+
gateway_base_url?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveConfig(config?: MCPHttpConfig): ResolvedMCPHttpConfig {
|
|
28
|
+
return {
|
|
29
|
+
platform: config?.platform || 'mcp_http',
|
|
30
|
+
gateway_base_url: config?.gateway_base_url,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Auth identity resolution
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
interface IdentityOverride {
|
|
39
|
+
workspace_id: string;
|
|
40
|
+
actor_id: string;
|
|
41
|
+
api_key_tags?: string[];
|
|
42
|
+
/** Raw OAuth access token — used for self-referencing gateway requests */
|
|
43
|
+
token?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the identity for a tool call from the authenticated request context.
|
|
48
|
+
*
|
|
49
|
+
* Handles two shapes:
|
|
50
|
+
* - Legacy (AuthContext from auth middleware): workspace_id/actor_id at top level
|
|
51
|
+
* - OAuth (MCP AuthInfo from requireBearerAuth): workspace_id/actor_id in .extra
|
|
52
|
+
*
|
|
53
|
+
* Returns undefined if authInfo is missing or incomplete. Callers must reject
|
|
54
|
+
* the request when identity is undefined — no fallback defaults are used.
|
|
55
|
+
*/
|
|
56
|
+
function resolveIdentity(extra?: { authInfo?: unknown }): IdentityOverride | undefined {
|
|
57
|
+
if (!extra?.authInfo) return undefined;
|
|
58
|
+
const authInfo = extra.authInfo as Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
// Try top-level (legacy AuthContext shape)
|
|
61
|
+
let wsId = authInfo.workspace_id;
|
|
62
|
+
let actorId = authInfo.actor_id;
|
|
63
|
+
|
|
64
|
+
// Fall back to .extra (MCP OAuth AuthInfo shape)
|
|
65
|
+
let apiKeyTags: string[] | undefined;
|
|
66
|
+
if ((!wsId || !actorId) && authInfo.extra && typeof authInfo.extra === 'object') {
|
|
67
|
+
const extraObj = authInfo.extra as Record<string, unknown>;
|
|
68
|
+
wsId = wsId || extraObj.workspace_id;
|
|
69
|
+
actorId = actorId || extraObj.actor_id;
|
|
70
|
+
if (Array.isArray(extraObj.api_key_tags)) apiKeyTags = extraObj.api_key_tags as string[];
|
|
71
|
+
}
|
|
72
|
+
// Also check top-level api_key_tags (legacy AuthContext shape)
|
|
73
|
+
if (!apiKeyTags && Array.isArray(authInfo.api_key_tags)) {
|
|
74
|
+
apiKeyTags = authInfo.api_key_tags as string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof wsId === 'string' && wsId && typeof actorId === 'string' && actorId) {
|
|
78
|
+
const token = typeof authInfo.token === 'string' ? authInfo.token : undefined;
|
|
79
|
+
return { workspace_id: wsId, actor_id: actorId, api_key_tags: apiKeyTags, token };
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Shared helpers (same patterns as bridge.ts)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function methodToCapability(method: string): ToolInfo['capability'] {
|
|
89
|
+
switch (method.toUpperCase()) {
|
|
90
|
+
case 'GET':
|
|
91
|
+
case 'HEAD':
|
|
92
|
+
case 'OPTIONS':
|
|
93
|
+
return 'read';
|
|
94
|
+
case 'POST':
|
|
95
|
+
case 'PUT':
|
|
96
|
+
case 'PATCH':
|
|
97
|
+
return 'write';
|
|
98
|
+
case 'DELETE':
|
|
99
|
+
return 'delete';
|
|
100
|
+
default:
|
|
101
|
+
return 'write';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseBody(body: string): unknown {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(body);
|
|
108
|
+
} catch {
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildToolCall(
|
|
114
|
+
config: ResolvedMCPHttpConfig,
|
|
115
|
+
params: {
|
|
116
|
+
toolName: string;
|
|
117
|
+
capability: ToolInfo['capability'];
|
|
118
|
+
args: ToolCallArgs;
|
|
119
|
+
constraints?: { timeout_ms?: number; max_cost_usd?: number };
|
|
120
|
+
context?: { purpose?: string; labels?: string[] };
|
|
121
|
+
identity: IdentityOverride;
|
|
122
|
+
},
|
|
123
|
+
): ToolCall {
|
|
124
|
+
const wsId = params.identity.workspace_id;
|
|
125
|
+
const actorId = params.identity.actor_id;
|
|
126
|
+
const toolCall: ToolCall = {
|
|
127
|
+
tool_call_id: randomUUID(),
|
|
128
|
+
task_id: randomUUID(),
|
|
129
|
+
workspace_id: wsId,
|
|
130
|
+
actor: { type: 'agent', id: actorId, display: actorId },
|
|
131
|
+
source: { platform: config.platform },
|
|
132
|
+
tool: {
|
|
133
|
+
name: params.toolName,
|
|
134
|
+
version: '1.0.0',
|
|
135
|
+
capability: params.capability,
|
|
136
|
+
},
|
|
137
|
+
args: params.args,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (params.constraints?.timeout_ms != null || params.constraints?.max_cost_usd != null) {
|
|
142
|
+
toolCall.constraints = {};
|
|
143
|
+
if (params.constraints.timeout_ms != null) {
|
|
144
|
+
toolCall.constraints.timeout_ms = params.constraints.timeout_ms;
|
|
145
|
+
}
|
|
146
|
+
if (params.constraints.max_cost_usd != null) {
|
|
147
|
+
toolCall.constraints.max_cost_usd = params.constraints.max_cost_usd;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (params.context?.purpose || params.context?.labels) {
|
|
152
|
+
toolCall.context = {};
|
|
153
|
+
if (params.context.purpose) {
|
|
154
|
+
toolCall.context.purpose = params.context.purpose;
|
|
155
|
+
}
|
|
156
|
+
if (params.context.labels) {
|
|
157
|
+
toolCall.context.labels = params.context.labels;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Merge API key tags into context.labels
|
|
162
|
+
if (params.identity.api_key_tags && params.identity.api_key_tags.length > 0) {
|
|
163
|
+
if (!toolCall.context) toolCall.context = {};
|
|
164
|
+
const existing = toolCall.context.labels || [];
|
|
165
|
+
toolCall.context.labels = [...new Set([...existing, ...params.identity.api_key_tags])];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return toolCall;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Store internal auth token for self-referencing gateway requests. */
|
|
172
|
+
function setInternalAuth(
|
|
173
|
+
config: ResolvedMCPHttpConfig,
|
|
174
|
+
identity: IdentityOverride,
|
|
175
|
+
toolCall: ToolCall,
|
|
176
|
+
targetUrl: string,
|
|
177
|
+
): void {
|
|
178
|
+
if (config.gateway_base_url && identity.token && targetUrl.startsWith(config.gateway_base_url)) {
|
|
179
|
+
internalAuthTokens.set(toolCall.tool_call_id, {
|
|
180
|
+
token: identity.token,
|
|
181
|
+
gateway_base_url: config.gateway_base_url,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatResult(result: ToolResult): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } {
|
|
187
|
+
const isError = result.status === 'error' || result.status === 'blocked';
|
|
188
|
+
|
|
189
|
+
let primaryText: string;
|
|
190
|
+
if (result.error) {
|
|
191
|
+
primaryText = result.error;
|
|
192
|
+
} else if (result.output?.body !== undefined) {
|
|
193
|
+
primaryText =
|
|
194
|
+
typeof result.output.body === 'string'
|
|
195
|
+
? result.output.body
|
|
196
|
+
: JSON.stringify(result.output.body, null, 2);
|
|
197
|
+
} else {
|
|
198
|
+
primaryText = `Request completed with status: ${result.status}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const metadata = {
|
|
202
|
+
tool_call_id: result.tool_call_id,
|
|
203
|
+
task_id: result.task_id,
|
|
204
|
+
status: result.status,
|
|
205
|
+
policy: result.policy,
|
|
206
|
+
dlp: {
|
|
207
|
+
detected: result.dlp.detected,
|
|
208
|
+
severity: result.dlp.severity,
|
|
209
|
+
redaction_count: result.dlp.redactions.length,
|
|
210
|
+
},
|
|
211
|
+
budget: result.budget,
|
|
212
|
+
timing: result.timing,
|
|
213
|
+
http_status: result.output?.http_status,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{ type: 'text', text: primaryText },
|
|
219
|
+
{ type: 'text', text: `--- Gateway Metadata ---\n${JSON.stringify(metadata, null, 2)}` },
|
|
220
|
+
],
|
|
221
|
+
isError,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Auth & capability helpers
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/** Build a standard MCP error result for auth failures. */
|
|
230
|
+
function authRequiredError() {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Authentication required. Provide valid credentials.', error_code: 'AUTH_REQUIRED' }) }],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check capability-level RBAC permission from the authenticated context.
|
|
239
|
+
* Returns an error message if permission is denied, or null if allowed.
|
|
240
|
+
* When authInfo has no permissions array (e.g. auth disabled), returns null (allow).
|
|
241
|
+
*
|
|
242
|
+
* Handles both legacy AuthContext (permissions at top level) and
|
|
243
|
+
* MCP OAuth AuthInfo (permissions in .extra).
|
|
244
|
+
*/
|
|
245
|
+
function checkCapabilityPermission(extra: { authInfo?: unknown }, capability: string): string | null {
|
|
246
|
+
if (!extra?.authInfo) return null;
|
|
247
|
+
const authInfo = extra.authInfo as Record<string, unknown>;
|
|
248
|
+
|
|
249
|
+
// Try top-level permissions (legacy), then .extra.permissions (OAuth)
|
|
250
|
+
let permissions = authInfo.permissions as string[] | undefined;
|
|
251
|
+
if (!permissions && authInfo.extra && typeof authInfo.extra === 'object') {
|
|
252
|
+
permissions = (authInfo.extra as Record<string, unknown>).permissions as string[] | undefined;
|
|
253
|
+
}
|
|
254
|
+
if (!permissions || permissions.length === 0) return null;
|
|
255
|
+
|
|
256
|
+
const required = `tool:execute:${capability}`;
|
|
257
|
+
const hasPermission = permissions.includes(required) ||
|
|
258
|
+
permissions.includes('tool:execute') ||
|
|
259
|
+
permissions.includes('admin:full');
|
|
260
|
+
if (!hasPermission) {
|
|
261
|
+
return `Permission denied: requires ${required}`;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Build a standard MCP error result for permission failures. */
|
|
267
|
+
function permissionDeniedError(message: string) {
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ error: message, error_code: 'AUTH_INSUFFICIENT_PERMS' }) }],
|
|
270
|
+
isError: true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Approval polling — wait for human approval then re-execute
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
const APPROVAL_POLL_INTERVAL_MS = 3000;
|
|
279
|
+
const APPROVAL_POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* If a tool call returns `needs_approval`, poll the approval store until it's
|
|
283
|
+
* resolved (approved/denied/expired) or we hit the timeout. On approval,
|
|
284
|
+
* re-execute the original tool call — the gateway will find the existing
|
|
285
|
+
* approval via `findApprovedForTask` and bypass the approval step.
|
|
286
|
+
*/
|
|
287
|
+
async function executeWithApprovalPolling(
|
|
288
|
+
gateway: Gateway,
|
|
289
|
+
toolCall: ToolCall,
|
|
290
|
+
): Promise<ToolResult> {
|
|
291
|
+
const result = await gateway.execute(toolCall);
|
|
292
|
+
|
|
293
|
+
const approvalInfo = (result as any).approval;
|
|
294
|
+
if (result.status !== 'needs_approval' || !approvalInfo?.approval_id) {
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const approvalId = approvalInfo.approval_id as string;
|
|
299
|
+
const approvalMgr = gateway.getApprovalManager();
|
|
300
|
+
const deadline = Date.now() + APPROVAL_POLL_TIMEOUT_MS;
|
|
301
|
+
|
|
302
|
+
// Poll until resolved or timeout
|
|
303
|
+
while (Date.now() < deadline) {
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, APPROVAL_POLL_INTERVAL_MS));
|
|
305
|
+
|
|
306
|
+
const approval = approvalMgr.getApproval(approvalId);
|
|
307
|
+
if (!approval) break; // approval disappeared
|
|
308
|
+
|
|
309
|
+
if (approval.status === 'approved') {
|
|
310
|
+
// Re-execute — gateway.preExecute will find the existing approval
|
|
311
|
+
// via findApprovedForTask and allow the call through
|
|
312
|
+
return gateway.execute(toolCall);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (approval.status === 'denied') {
|
|
316
|
+
return {
|
|
317
|
+
...result,
|
|
318
|
+
status: 'blocked',
|
|
319
|
+
error: `Request denied by ${approval.resolved_by || 'approver'}${approval.denial_reason ? ': ' + approval.denial_reason : ''}`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (approval.status === 'expired') {
|
|
324
|
+
return {
|
|
325
|
+
...result,
|
|
326
|
+
status: 'blocked',
|
|
327
|
+
error: 'Approval request expired before it was resolved.',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Timeout — return the original needs_approval result so the caller knows
|
|
333
|
+
return {
|
|
334
|
+
...result,
|
|
335
|
+
error: 'Approval polling timed out after 5 minutes. Approve via the admin panel and retry.',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// MCP Server factory — creates a new McpServer with tools wired to Gateway
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
function createMCPServerInstance(gateway: Gateway, config: ResolvedMCPHttpConfig): McpServer {
|
|
344
|
+
const server = new McpServer(
|
|
345
|
+
{ name: 'palaryn-mcp', version: '1.0.0' },
|
|
346
|
+
{ capabilities: { tools: {} } },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Shared Zod schemas for tool input
|
|
350
|
+
const headersSchema = z.record(z.string(), z.string()).optional().describe('HTTP headers as key-value pairs');
|
|
351
|
+
const timeoutSchema = z.number().optional().describe('Request timeout in milliseconds');
|
|
352
|
+
const costSchema = z.number().optional().describe('Maximum cost budget for this request in USD');
|
|
353
|
+
const purposeSchema = z.string().optional().describe('Description of why this request is being made');
|
|
354
|
+
const labelsSchema = z.array(z.string()).optional().describe('Classification labels for the request');
|
|
355
|
+
|
|
356
|
+
// Tool args are typed as Record<string, unknown> due to Zod v4 generic inference
|
|
357
|
+
// through the MCP SDK. We extract fields with runtime checks matching bridge.ts.
|
|
358
|
+
type ToolArgs = Record<string, unknown>;
|
|
359
|
+
|
|
360
|
+
function extractCommon(a: ToolArgs) {
|
|
361
|
+
return {
|
|
362
|
+
constraints: {
|
|
363
|
+
timeout_ms: typeof a.timeout_ms === 'number' ? a.timeout_ms : undefined,
|
|
364
|
+
max_cost_usd: typeof a.max_cost_usd === 'number' ? a.max_cost_usd : undefined,
|
|
365
|
+
},
|
|
366
|
+
context: {
|
|
367
|
+
purpose: typeof a.purpose === 'string' ? a.purpose : undefined,
|
|
368
|
+
labels: Array.isArray(a.labels) ? a.labels : undefined,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// --- http_request: arbitrary HTTP method ---
|
|
374
|
+
server.registerTool(
|
|
375
|
+
'http_request',
|
|
376
|
+
{
|
|
377
|
+
description:
|
|
378
|
+
'Execute an HTTP request through the Palaryn gateway. ' +
|
|
379
|
+
'Supports all HTTP methods. The request goes through policy evaluation, ' +
|
|
380
|
+
'DLP scanning, budget checks, and rate limiting before execution.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
url: z.string().describe('The target URL for the HTTP request'),
|
|
383
|
+
method: z
|
|
384
|
+
.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])
|
|
385
|
+
.optional()
|
|
386
|
+
.describe('HTTP method (defaults to GET)'),
|
|
387
|
+
headers: headersSchema,
|
|
388
|
+
body: z.string().optional().describe('Request body (typically JSON string for POST/PUT/PATCH)'),
|
|
389
|
+
timeout_ms: timeoutSchema,
|
|
390
|
+
max_cost_usd: costSchema,
|
|
391
|
+
purpose: purposeSchema,
|
|
392
|
+
labels: labelsSchema,
|
|
393
|
+
},
|
|
394
|
+
annotations: { title: 'HTTP Request', readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
395
|
+
},
|
|
396
|
+
async (a: ToolArgs, extra: { authInfo?: unknown }) => {
|
|
397
|
+
const identity = resolveIdentity(extra);
|
|
398
|
+
if (!identity) return authRequiredError();
|
|
399
|
+
|
|
400
|
+
const method = (typeof a.method === 'string' ? a.method : 'GET').toUpperCase();
|
|
401
|
+
const capability = methodToCapability(method);
|
|
402
|
+
const capError = checkCapabilityPermission(extra, capability);
|
|
403
|
+
if (capError) return permissionDeniedError(capError);
|
|
404
|
+
|
|
405
|
+
const { constraints, context } = extractCommon(a);
|
|
406
|
+
const toolCall = buildToolCall(config, {
|
|
407
|
+
toolName: 'http.request',
|
|
408
|
+
capability,
|
|
409
|
+
args: {
|
|
410
|
+
method,
|
|
411
|
+
url: a.url as string,
|
|
412
|
+
headers: a.headers as Record<string, string> | undefined,
|
|
413
|
+
body: typeof a.body === 'string' ? parseBody(a.body) : undefined,
|
|
414
|
+
},
|
|
415
|
+
constraints,
|
|
416
|
+
context,
|
|
417
|
+
identity,
|
|
418
|
+
});
|
|
419
|
+
setInternalAuth(config, identity, toolCall, a.url as string);
|
|
420
|
+
return formatResult(await executeWithApprovalPolling(gateway, toolCall));
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// --- http_get: GET shorthand ---
|
|
425
|
+
server.registerTool(
|
|
426
|
+
'http_get',
|
|
427
|
+
{
|
|
428
|
+
description:
|
|
429
|
+
'Execute an HTTP GET request through the Palaryn gateway. ' +
|
|
430
|
+
'Shorthand for http_request with method=GET. The request goes through ' +
|
|
431
|
+
'policy evaluation, DLP scanning, budget checks, and rate limiting.',
|
|
432
|
+
inputSchema: {
|
|
433
|
+
url: z.string().describe('The target URL for the GET request'),
|
|
434
|
+
headers: headersSchema,
|
|
435
|
+
timeout_ms: timeoutSchema,
|
|
436
|
+
max_cost_usd: costSchema,
|
|
437
|
+
purpose: purposeSchema,
|
|
438
|
+
labels: labelsSchema,
|
|
439
|
+
},
|
|
440
|
+
annotations: { title: 'HTTP GET', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
441
|
+
},
|
|
442
|
+
async (a: ToolArgs, extra: { authInfo?: unknown }) => {
|
|
443
|
+
const identity = resolveIdentity(extra);
|
|
444
|
+
if (!identity) return authRequiredError();
|
|
445
|
+
|
|
446
|
+
const capError = checkCapabilityPermission(extra, 'read');
|
|
447
|
+
if (capError) return permissionDeniedError(capError);
|
|
448
|
+
|
|
449
|
+
const { constraints, context } = extractCommon(a);
|
|
450
|
+
const toolCall = buildToolCall(config, {
|
|
451
|
+
toolName: 'http.get',
|
|
452
|
+
capability: 'read',
|
|
453
|
+
args: {
|
|
454
|
+
method: 'GET',
|
|
455
|
+
url: a.url as string,
|
|
456
|
+
headers: a.headers as Record<string, string> | undefined,
|
|
457
|
+
},
|
|
458
|
+
constraints,
|
|
459
|
+
context,
|
|
460
|
+
identity,
|
|
461
|
+
});
|
|
462
|
+
setInternalAuth(config, identity, toolCall, a.url as string);
|
|
463
|
+
return formatResult(await executeWithApprovalPolling(gateway, toolCall));
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// --- http_post: POST shorthand ---
|
|
468
|
+
server.registerTool(
|
|
469
|
+
'http_post',
|
|
470
|
+
{
|
|
471
|
+
description:
|
|
472
|
+
'Execute an HTTP POST request through the Palaryn gateway. ' +
|
|
473
|
+
'Shorthand for http_request with method=POST. The request goes through ' +
|
|
474
|
+
'policy evaluation, DLP scanning, budget checks, and rate limiting.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
url: z.string().describe('The target URL for the POST request'),
|
|
477
|
+
headers: headersSchema,
|
|
478
|
+
body: z.string().optional().describe('Request body (typically a JSON string)'),
|
|
479
|
+
timeout_ms: timeoutSchema,
|
|
480
|
+
max_cost_usd: costSchema,
|
|
481
|
+
purpose: purposeSchema,
|
|
482
|
+
labels: labelsSchema,
|
|
483
|
+
},
|
|
484
|
+
annotations: { title: 'HTTP POST', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
485
|
+
},
|
|
486
|
+
async (a: ToolArgs, extra: { authInfo?: unknown }) => {
|
|
487
|
+
const identity = resolveIdentity(extra);
|
|
488
|
+
if (!identity) return authRequiredError();
|
|
489
|
+
|
|
490
|
+
const capError = checkCapabilityPermission(extra, 'write');
|
|
491
|
+
if (capError) return permissionDeniedError(capError);
|
|
492
|
+
|
|
493
|
+
const { constraints, context } = extractCommon(a);
|
|
494
|
+
const toolCall = buildToolCall(config, {
|
|
495
|
+
toolName: 'http.post',
|
|
496
|
+
capability: 'write',
|
|
497
|
+
args: {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
url: a.url as string,
|
|
500
|
+
headers: a.headers as Record<string, string> | undefined,
|
|
501
|
+
body: typeof a.body === 'string' ? parseBody(a.body) : undefined,
|
|
502
|
+
},
|
|
503
|
+
constraints,
|
|
504
|
+
context,
|
|
505
|
+
identity,
|
|
506
|
+
});
|
|
507
|
+
setInternalAuth(config, identity, toolCall, a.url as string);
|
|
508
|
+
return formatResult(await executeWithApprovalPolling(gateway, toolCall));
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
return server;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// Express router factory
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Creates an Express router that serves an MCP HTTP transport endpoint.
|
|
521
|
+
*
|
|
522
|
+
* Uses the Streamable HTTP transport from the MCP SDK with stateful sessions.
|
|
523
|
+
* Each MCP client session gets its own transport and server instance.
|
|
524
|
+
*
|
|
525
|
+
* Auth middleware must always be mounted upstream of this router. The transport
|
|
526
|
+
* reads authenticated identity from `extra.authInfo` (set by the MCP SDK from
|
|
527
|
+
* `req.auth`). When auth is disabled globally, the auth middleware still sets
|
|
528
|
+
* an anonymous identity which RBAC allows through.
|
|
529
|
+
*
|
|
530
|
+
* Mount at `/mcp`:
|
|
531
|
+
* ```typescript
|
|
532
|
+
* const { router } = createMCPHttpHandler(gateway);
|
|
533
|
+
* app.use('/mcp', authMiddleware, rbacToolExecute, router);
|
|
534
|
+
* ```
|
|
535
|
+
*
|
|
536
|
+
* Clients connect via: `claude mcp add palaryn --url https://host/mcp`
|
|
537
|
+
*/
|
|
538
|
+
export const MAX_SESSIONS = 1000;
|
|
539
|
+
export const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
540
|
+
|
|
541
|
+
export function createMCPHttpHandler(
|
|
542
|
+
gateway: Gateway,
|
|
543
|
+
config?: MCPHttpConfig,
|
|
544
|
+
): { router: express.Router; close: () => Promise<void> } {
|
|
545
|
+
const resolvedConfig = resolveConfig(config);
|
|
546
|
+
const sessions = new Map<string, { transport: StreamableHTTPServerTransport; server: McpServer }>();
|
|
547
|
+
const sessionActivity = new Map<string, number>();
|
|
548
|
+
|
|
549
|
+
// Periodic cleanup of idle sessions
|
|
550
|
+
const cleanupInterval = setInterval(() => {
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
for (const [id, lastActive] of sessionActivity) {
|
|
553
|
+
if (now - lastActive > SESSION_TIMEOUT_MS) {
|
|
554
|
+
const session = sessions.get(id);
|
|
555
|
+
if (session) {
|
|
556
|
+
session.server.close().catch(() => {});
|
|
557
|
+
sessions.delete(id);
|
|
558
|
+
sessionActivity.delete(id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}, 60000);
|
|
563
|
+
cleanupInterval.unref();
|
|
564
|
+
|
|
565
|
+
const router = express.Router();
|
|
566
|
+
|
|
567
|
+
// POST /mcp — initialize new session or send requests to existing session
|
|
568
|
+
router.post('/', async (req, res) => {
|
|
569
|
+
try {
|
|
570
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
571
|
+
|
|
572
|
+
if (sessionId) {
|
|
573
|
+
// Session ID provided — route to existing session
|
|
574
|
+
const session = sessions.get(sessionId);
|
|
575
|
+
if (!session) {
|
|
576
|
+
res.status(404).json({
|
|
577
|
+
jsonrpc: '2.0',
|
|
578
|
+
error: { code: -32000, message: 'Session not found. The session may have expired.' },
|
|
579
|
+
id: null,
|
|
580
|
+
});
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
sessionActivity.set(sessionId, Date.now());
|
|
584
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Enforce session limit — evict oldest inactive session if at capacity
|
|
589
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
590
|
+
let oldestId: string | null = null;
|
|
591
|
+
let oldestTime = Infinity;
|
|
592
|
+
for (const [id, time] of sessionActivity) {
|
|
593
|
+
if (time < oldestTime) {
|
|
594
|
+
oldestTime = time;
|
|
595
|
+
oldestId = id;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (oldestId) {
|
|
599
|
+
const old = sessions.get(oldestId);
|
|
600
|
+
if (old) old.server.close().catch(() => {});
|
|
601
|
+
sessions.delete(oldestId);
|
|
602
|
+
sessionActivity.delete(oldestId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// No session ID — create new session (initialize request)
|
|
607
|
+
const transport = new StreamableHTTPServerTransport({
|
|
608
|
+
sessionIdGenerator: () => randomUUID(),
|
|
609
|
+
onsessioninitialized: (id) => {
|
|
610
|
+
sessions.set(id, { transport, server });
|
|
611
|
+
sessionActivity.set(id, Date.now());
|
|
612
|
+
},
|
|
613
|
+
onsessionclosed: (id) => {
|
|
614
|
+
sessions.delete(id);
|
|
615
|
+
sessionActivity.delete(id);
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
const server = createMCPServerInstance(gateway, resolvedConfig);
|
|
619
|
+
await server.connect(transport);
|
|
620
|
+
await transport.handleRequest(req, res, req.body);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
if (!res.headersSent) {
|
|
623
|
+
console.error('[mcp] POST /mcp error:', err);
|
|
624
|
+
res.status(500).json({
|
|
625
|
+
jsonrpc: '2.0',
|
|
626
|
+
error: { code: -32603, message: 'Internal error' },
|
|
627
|
+
id: null,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// GET /mcp — SSE stream for server-to-client notifications
|
|
634
|
+
router.get('/', async (req, res) => {
|
|
635
|
+
try {
|
|
636
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
637
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
638
|
+
res.status(400).json({
|
|
639
|
+
jsonrpc: '2.0',
|
|
640
|
+
error: { code: -32000, message: 'Invalid or missing session ID.' },
|
|
641
|
+
id: null,
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
sessionActivity.set(sessionId, Date.now());
|
|
646
|
+
const session = sessions.get(sessionId)!;
|
|
647
|
+
await session.transport.handleRequest(req, res);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (!res.headersSent) {
|
|
650
|
+
console.error('[mcp] GET /mcp error:', err);
|
|
651
|
+
res.status(500).json({
|
|
652
|
+
jsonrpc: '2.0',
|
|
653
|
+
error: { code: -32603, message: 'Internal error' },
|
|
654
|
+
id: null,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// DELETE /mcp — session cleanup
|
|
661
|
+
router.delete('/', async (req, res) => {
|
|
662
|
+
try {
|
|
663
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
664
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
665
|
+
res.status(404).json({
|
|
666
|
+
jsonrpc: '2.0',
|
|
667
|
+
error: { code: -32000, message: 'Session not found.' },
|
|
668
|
+
id: null,
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const session = sessions.get(sessionId)!;
|
|
673
|
+
await session.transport.handleRequest(req, res);
|
|
674
|
+
// onsessionclosed callback handles map cleanup
|
|
675
|
+
} catch (err) {
|
|
676
|
+
if (!res.headersSent) {
|
|
677
|
+
console.error('[mcp] DELETE /mcp error:', err);
|
|
678
|
+
res.status(500).json({
|
|
679
|
+
jsonrpc: '2.0',
|
|
680
|
+
error: { code: -32603, message: 'Internal error' },
|
|
681
|
+
id: null,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Close all sessions — call on server shutdown
|
|
688
|
+
const close = async () => {
|
|
689
|
+
clearInterval(cleanupInterval);
|
|
690
|
+
for (const [, session] of sessions) {
|
|
691
|
+
await session.server.close();
|
|
692
|
+
}
|
|
693
|
+
sessions.clear();
|
|
694
|
+
sessionActivity.clear();
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
return { router, close };
|
|
698
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { MCPBridge, startMCPBridge } from './bridge';
|
|
2
|
+
export type { MCPBridgeConfig } from './bridge';
|
|
3
|
+
export { createMCPHttpHandler } from './http-transport';
|
|
4
|
+
export type { MCPHttpConfig } from './http-transport';
|
|
5
|
+
export { PalarynOAuthProvider } from './oauth-provider';
|
|
6
|
+
export type { PalarynOAuthProviderDeps } from './oauth-provider';
|
|
7
|
+
export { HybridTokenVerifier } from './auth-verifier';
|
|
8
|
+
export type { HybridVerifierDeps } from './auth-verifier';
|
|
9
|
+
export { OAuthClientsStore, AuthCodeStore, OAuthTokenStore } from './oauth-stores';
|