@casys/mcp-server 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mod.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * MCP Concurrent Server Framework
3
+ *
4
+ * Production-ready MCP server framework with built-in concurrency control,
5
+ * backpressure strategies, and optional sampling support.
6
+ *
7
+ * Built on top of the official @modelcontextprotocol/sdk with added
8
+ * production features for reliability and performance.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { ConcurrentMCPServer } from "@casys/mcp-server";
13
+ *
14
+ * const server = new ConcurrentMCPServer({
15
+ * name: "my-server",
16
+ * version: "1.0.0",
17
+ * maxConcurrent: 10,
18
+ * backpressureStrategy: 'queue'
19
+ * });
20
+ *
21
+ * server.registerTool(
22
+ * { name: "greet", description: "Greet someone", inputSchema: { type: "object" } },
23
+ * (args) => `Hello, ${args.name}!`,
24
+ * );
25
+ *
26
+ * // STDIO transport
27
+ * await server.start();
28
+ *
29
+ * // — or — HTTP transport with security-first defaults
30
+ * const http = await server.startHttp({
31
+ * port: 3000,
32
+ * maxBodyBytes: 1_048_576, // 1 MB (default)
33
+ * corsOrigins: ["https://app.example.com"], // allowlist
34
+ * requireAuth: true, // fail-fast without auth
35
+ * ipRateLimit: { maxRequests: 60, windowMs: 60_000 },
36
+ * });
37
+ * ```
38
+ *
39
+ * @module @casys/mcp-server
40
+ */
41
+
42
+ // Main server class
43
+ export { ConcurrentMCPServer } from "./src/concurrent-server.js";
44
+
45
+ // Concurrency primitives
46
+ export { RequestQueue } from "./src/concurrency/request-queue.js";
47
+
48
+ // Rate limiting
49
+ export { RateLimiter } from "./src/concurrency/rate-limiter.js";
50
+
51
+ // Schema validation
52
+ export { SchemaValidator } from "./src/validation/schema-validator.js";
53
+ export type {
54
+ ValidationError,
55
+ ValidationResult,
56
+ } from "./src/validation/schema-validator.js";
57
+
58
+ // Sampling support
59
+ export { SamplingBridge } from "./src/sampling/sampling-bridge.js";
60
+
61
+ // Type exports
62
+ export type {
63
+ ConcurrentServerOptions,
64
+ HttpRateLimitContext,
65
+ HttpRateLimitOptions,
66
+ HttpServerInstance,
67
+ // HTTP Server types
68
+ HttpServerOptions,
69
+ // MCP Apps types (SEP-1865)
70
+ MCPResource,
71
+ MCPTool,
72
+ MCPToolMeta,
73
+ McpUiToolMeta,
74
+ QueueMetrics,
75
+ RateLimitContext,
76
+ RateLimitOptions,
77
+ ResourceContent,
78
+ ResourceHandler,
79
+ SamplingClient,
80
+ SamplingParams,
81
+ SamplingResult,
82
+ ToolHandler,
83
+ } from "./src/types.js";
84
+
85
+ // MCP Apps constants
86
+ export { MCP_APP_MIME_TYPE } from "./src/types.js";
87
+
88
+ // Middleware pipeline
89
+ export type {
90
+ Middleware,
91
+ MiddlewareContext,
92
+ MiddlewareResult,
93
+ NextFunction,
94
+ } from "./src/middleware/mod.js";
95
+ export { createMiddlewareRunner } from "./src/middleware/mod.js";
96
+
97
+ // Auth - Core
98
+ export { AuthProvider } from "./src/auth/mod.js";
99
+ export {
100
+ AuthError,
101
+ createAuthMiddleware,
102
+ createForbiddenResponse,
103
+ createUnauthorizedResponse,
104
+ extractBearerToken,
105
+ } from "./src/auth/mod.js";
106
+ export { createScopeMiddleware } from "./src/auth/mod.js";
107
+ export type {
108
+ AuthInfo,
109
+ AuthOptions,
110
+ ProtectedResourceMetadata,
111
+ } from "./src/auth/mod.js";
112
+
113
+ // Auth - JWT Provider + Presets
114
+ export { JwtAuthProvider } from "./src/auth/mod.js";
115
+ export type { JwtAuthProviderOptions } from "./src/auth/mod.js";
116
+ export {
117
+ createAuth0AuthProvider,
118
+ createGitHubAuthProvider,
119
+ createGoogleAuthProvider,
120
+ createOIDCAuthProvider,
121
+ } from "./src/auth/mod.js";
122
+ export type { PresetOptions } from "./src/auth/mod.js";
123
+
124
+ // Auth - Config (YAML + env)
125
+ export {
126
+ createAuthProviderFromConfig,
127
+ loadAuthConfig,
128
+ } from "./src/auth/mod.js";
129
+ export type { AuthConfig, AuthProviderName } from "./src/auth/mod.js";
130
+
131
+ // Observability
132
+ export {
133
+ endToolCallSpan,
134
+ getServerTracer,
135
+ isOtelEnabled,
136
+ recordAuthEvent,
137
+ ServerMetrics,
138
+ type ServerMetricsSnapshot,
139
+ startToolCallSpan,
140
+ type ToolCallSpanAttributes,
141
+ } from "./src/observability/mod.js";
142
+
143
+ // Security - CSP utilities
144
+ export { buildCspHeader, injectCspMetaTag } from "./src/security/csp.js";
145
+ export type { CspOptions } from "./src/security/csp.js";
146
+
147
+ // Security - HMAC channel authentication for PostMessage (MCP Apps)
148
+ export { injectChannelAuth } from "./src/security/channel-hmac.js";
149
+ export { MessageSigner } from "./src/security/message-signer.js";
150
+ export type {
151
+ SignedMessage,
152
+ VerifyResult,
153
+ } from "./src/security/message-signer.js";
154
+
155
+ // Runtime port (for advanced consumers who need to inspect the adapter contract)
156
+ export type {
157
+ FetchHandler,
158
+ RuntimePort,
159
+ ServeHandle,
160
+ ServeOptions,
161
+ } from "./src/runtime/types.js";
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@casys/mcp-server",
3
+ "version": "0.8.0",
4
+ "description": "Production-ready MCP server framework with concurrency control, auth, and observability",
5
+ "type": "module",
6
+ "main": "mod.ts",
7
+ "types": "mod.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "tsx --test src/**/*_test.ts"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.15.1",
14
+ "hono": "^4.0.0",
15
+ "ajv": "^8.17.1",
16
+ "jose": "^6.0.0",
17
+ "yaml": "^2.7.0",
18
+ "@opentelemetry/api": "^1.9.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.4.0",
22
+ "tsx": "^4.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Casys-AI/mcp-server"
30
+ },
31
+ "license": "MIT"
32
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Auth configuration loader.
3
+ *
4
+ * Loads auth config from YAML file with env var overrides.
5
+ * Priority: env vars > YAML > (nothing = no auth)
6
+ *
7
+ * @module lib/server/auth/config
8
+ */
9
+
10
+ import { parse as parseYaml } from "yaml";
11
+ import { env, readTextFile } from "../runtime/runtime.js";
12
+ import type { AuthProvider } from "./provider.js";
13
+ import {
14
+ createAuth0AuthProvider,
15
+ createGitHubAuthProvider,
16
+ createGoogleAuthProvider,
17
+ createOIDCAuthProvider,
18
+ } from "./presets.js";
19
+
20
+ /** Supported auth provider names */
21
+ export type AuthProviderName = "github" | "google" | "auth0" | "oidc";
22
+
23
+ const VALID_PROVIDERS: AuthProviderName[] = [
24
+ "github",
25
+ "google",
26
+ "auth0",
27
+ "oidc",
28
+ ];
29
+
30
+ /**
31
+ * Parsed auth configuration (after YAML + env merge).
32
+ */
33
+ export interface AuthConfig {
34
+ provider: AuthProviderName;
35
+ audience: string;
36
+ resource: string;
37
+ /** Auth0 tenant domain */
38
+ domain?: string;
39
+ /** OIDC issuer */
40
+ issuer?: string;
41
+ /** OIDC JWKS URI (optional, derived from issuer if absent) */
42
+ jwksUri?: string;
43
+ /** Supported scopes */
44
+ scopesSupported?: string[];
45
+ }
46
+
47
+ /**
48
+ * YAML file schema (top-level has `auth` key).
49
+ */
50
+ interface ConfigFile {
51
+ auth?: {
52
+ provider?: string;
53
+ audience?: string;
54
+ resource?: string;
55
+ domain?: string;
56
+ issuer?: string;
57
+ jwksUri?: string;
58
+ scopesSupported?: string[];
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Load auth configuration from YAML file + env var overrides.
64
+ *
65
+ * 1. Reads YAML file (if it exists)
66
+ * 2. Overlays env vars (MCP_AUTH_* take precedence)
67
+ * 3. Validates the merged config
68
+ * 4. Returns null if no auth is configured
69
+ *
70
+ * Env var mapping:
71
+ * - MCP_AUTH_PROVIDER → auth.provider
72
+ * - MCP_AUTH_AUDIENCE → auth.audience
73
+ * - MCP_AUTH_RESOURCE → auth.resource
74
+ * - MCP_AUTH_DOMAIN → auth.domain
75
+ * - MCP_AUTH_ISSUER → auth.issuer
76
+ * - MCP_AUTH_JWKS_URI → auth.jwksUri
77
+ * - MCP_AUTH_SCOPES → auth.scopesSupported (space-separated)
78
+ *
79
+ * @param configPath - Path to YAML config file. Defaults to "mcp-server.yaml" in cwd.
80
+ * @returns AuthConfig or null if no auth configured
81
+ * @throws Error if config is invalid (fail-fast)
82
+ */
83
+ export async function loadAuthConfig(
84
+ configPath?: string,
85
+ ): Promise<AuthConfig | null> {
86
+ // 1. Load YAML (optional - file may not exist)
87
+ const yamlAuth = await loadYamlAuth(configPath ?? "mcp-server.yaml");
88
+
89
+ // 2. Read env vars
90
+ const envProvider = env("MCP_AUTH_PROVIDER");
91
+ const envAudience = env("MCP_AUTH_AUDIENCE");
92
+ const envResource = env("MCP_AUTH_RESOURCE");
93
+ const envDomain = env("MCP_AUTH_DOMAIN");
94
+ const envIssuer = env("MCP_AUTH_ISSUER");
95
+ const envJwksUri = env("MCP_AUTH_JWKS_URI");
96
+ const envScopes = env("MCP_AUTH_SCOPES");
97
+
98
+ // 3. Merge: env overrides YAML
99
+ const provider = envProvider ?? yamlAuth?.provider;
100
+
101
+ // No provider configured anywhere → no auth
102
+ if (!provider) return null;
103
+
104
+ // Validate provider name
105
+ if (!VALID_PROVIDERS.includes(provider as AuthProviderName)) {
106
+ throw new Error(
107
+ `[AuthConfig] Unknown auth provider: "${provider}". ` +
108
+ `Valid values: ${VALID_PROVIDERS.join(", ")}`,
109
+ );
110
+ }
111
+
112
+ const audience = envAudience ?? yamlAuth?.audience;
113
+ const resource = envResource ?? yamlAuth?.resource;
114
+
115
+ if (!audience) {
116
+ throw new Error(
117
+ `[AuthConfig] "audience" is required when provider="${provider}". ` +
118
+ "Set auth.audience in YAML or MCP_AUTH_AUDIENCE env var.",
119
+ );
120
+ }
121
+ if (!resource) {
122
+ throw new Error(
123
+ `[AuthConfig] "resource" is required when provider="${provider}". ` +
124
+ "Set auth.resource in YAML or MCP_AUTH_RESOURCE env var.",
125
+ );
126
+ }
127
+
128
+ const config: AuthConfig = {
129
+ provider: provider as AuthProviderName,
130
+ audience,
131
+ resource,
132
+ domain: envDomain ?? yamlAuth?.domain,
133
+ issuer: envIssuer ?? yamlAuth?.issuer,
134
+ jwksUri: envJwksUri ?? yamlAuth?.jwksUri,
135
+ scopesSupported: envScopes
136
+ ? envScopes.split(" ").filter(Boolean)
137
+ : yamlAuth?.scopesSupported,
138
+ };
139
+
140
+ // Provider-specific validation (fail-fast)
141
+ if (config.provider === "auth0" && !config.domain) {
142
+ throw new Error(
143
+ '[AuthConfig] "domain" is required for auth0 provider. ' +
144
+ "Set auth.domain in YAML or MCP_AUTH_DOMAIN env var.",
145
+ );
146
+ }
147
+ if (config.provider === "oidc" && !config.issuer) {
148
+ throw new Error(
149
+ '[AuthConfig] "issuer" is required for oidc provider. ' +
150
+ "Set auth.issuer in YAML or MCP_AUTH_ISSUER env var.",
151
+ );
152
+ }
153
+
154
+ return config;
155
+ }
156
+
157
+ /**
158
+ * Create an AuthProvider from a loaded AuthConfig.
159
+ *
160
+ * @param config - Validated auth config
161
+ * @returns AuthProvider instance
162
+ */
163
+ export function createAuthProviderFromConfig(config: AuthConfig): AuthProvider {
164
+ const base = {
165
+ audience: config.audience,
166
+ resource: config.resource,
167
+ scopesSupported: config.scopesSupported,
168
+ };
169
+
170
+ switch (config.provider) {
171
+ case "github":
172
+ return createGitHubAuthProvider(base);
173
+ case "google":
174
+ return createGoogleAuthProvider(base);
175
+ case "auth0":
176
+ return createAuth0AuthProvider({ ...base, domain: config.domain! });
177
+ case "oidc":
178
+ return createOIDCAuthProvider({
179
+ ...base,
180
+ issuer: config.issuer!,
181
+ jwksUri: config.jwksUri,
182
+ authorizationServers: [config.issuer!],
183
+ });
184
+ default:
185
+ throw new Error(`[AuthConfig] Unsupported provider: ${config.provider}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Load the auth section from a YAML config file.
191
+ * Returns null if file doesn't exist (not an error).
192
+ */
193
+ async function loadYamlAuth(
194
+ path: string,
195
+ ): Promise<ConfigFile["auth"] | null> {
196
+ // readTextFile returns null if file doesn't exist
197
+ const text = await readTextFile(path);
198
+ if (text === null) return null;
199
+
200
+ const parsed = parseYaml(text);
201
+
202
+ // Validate parsed YAML is an object (not string, array, null, etc.)
203
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
204
+ return null;
205
+ }
206
+
207
+ const configFile = parsed as Record<string, unknown>;
208
+ if (
209
+ !configFile.auth || typeof configFile.auth !== "object" ||
210
+ Array.isArray(configFile.auth)
211
+ ) {
212
+ return null;
213
+ }
214
+
215
+ const auth = configFile.auth as Record<string, unknown>;
216
+ return {
217
+ provider: typeof auth.provider === "string" ? auth.provider : undefined,
218
+ audience: typeof auth.audience === "string" ? auth.audience : undefined,
219
+ resource: typeof auth.resource === "string" ? auth.resource : undefined,
220
+ domain: typeof auth.domain === "string" ? auth.domain : undefined,
221
+ issuer: typeof auth.issuer === "string" ? auth.issuer : undefined,
222
+ jwksUri: typeof auth.jwksUri === "string" ? auth.jwksUri : undefined,
223
+ scopesSupported: Array.isArray(auth.scopesSupported)
224
+ ? auth.scopesSupported.filter((s: unknown): s is string =>
225
+ typeof s === "string"
226
+ )
227
+ : undefined,
228
+ };
229
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * JWT Auth Provider using JWKS for token validation.
3
+ *
4
+ * Validates JWT tokens against a remote JWKS endpoint,
5
+ * checking issuer, audience, and expiration.
6
+ *
7
+ * @module lib/server/auth/jwt-provider
8
+ */
9
+
10
+ import { createRemoteJWKSet, jwtVerify } from "jose";
11
+ import { AuthProvider } from "./provider.js";
12
+ import type { AuthInfo, ProtectedResourceMetadata } from "./types.js";
13
+ import { isOtelEnabled, recordAuthEvent } from "../observability/otel.js";
14
+
15
+ /**
16
+ * Configuration for JwtAuthProvider.
17
+ */
18
+ export interface JwtAuthProviderOptions {
19
+ /** JWT issuer (iss claim) */
20
+ issuer: string;
21
+ /** JWT audience (aud claim) */
22
+ audience: string;
23
+ /** JWKS URI for signature validation. Defaults to {issuer}/.well-known/jwks.json */
24
+ jwksUri?: string;
25
+ /** Resource identifier for RFC 9728 */
26
+ resource: string;
27
+ /** Authorization servers that issue valid tokens */
28
+ authorizationServers: string[];
29
+ /** Scopes supported by this server */
30
+ scopesSupported?: string[];
31
+ }
32
+
33
+ /**
34
+ * JWT Auth Provider with JWKS validation.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const provider = new JwtAuthProvider({
39
+ * issuer: "https://accounts.google.com",
40
+ * audience: "https://my-mcp.example.com",
41
+ * resource: "https://my-mcp.example.com",
42
+ * authorizationServers: ["https://accounts.google.com"],
43
+ * });
44
+ * ```
45
+ */
46
+ /**
47
+ * Cached auth result with expiration
48
+ */
49
+ interface CachedAuth {
50
+ authInfo: AuthInfo;
51
+ expiresAt: number; // ms timestamp
52
+ }
53
+
54
+ export class JwtAuthProvider extends AuthProvider {
55
+ private jwks: ReturnType<typeof createRemoteJWKSet>;
56
+ private options: JwtAuthProviderOptions;
57
+
58
+ // Token verification cache: hash(token) → AuthInfo with TTL
59
+ // Prevents redundant JWKS fetches (network round-trip per tool call)
60
+ private tokenCache = new Map<string, CachedAuth>();
61
+ private static readonly MAX_CACHE_SIZE = 1000;
62
+ private static readonly DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
63
+
64
+ constructor(options: JwtAuthProviderOptions) {
65
+ super();
66
+
67
+ if (!options.issuer) {
68
+ throw new Error("[JwtAuthProvider] issuer is required");
69
+ }
70
+ if (!options.audience) {
71
+ throw new Error("[JwtAuthProvider] audience is required");
72
+ }
73
+ if (!options.resource) {
74
+ throw new Error("[JwtAuthProvider] resource is required");
75
+ }
76
+ if (!options.authorizationServers?.length) {
77
+ throw new Error(
78
+ "[JwtAuthProvider] at least one authorizationServer is required",
79
+ );
80
+ }
81
+
82
+ this.options = options;
83
+ const jwksUri = options.jwksUri ??
84
+ `${options.issuer}/.well-known/jwks.json`;
85
+ this.jwks = createRemoteJWKSet(new URL(jwksUri));
86
+ }
87
+
88
+ async verifyToken(token: string): Promise<AuthInfo | null> {
89
+ // Check cache first (avoids JWKS network round-trip)
90
+ const cacheKey = await this.hashToken(token);
91
+ const cached = this.tokenCache.get(cacheKey);
92
+ if (cached && Date.now() < cached.expiresAt) {
93
+ if (isOtelEnabled()) {
94
+ recordAuthEvent("cache_hit", {
95
+ subject: cached.authInfo.subject ?? "",
96
+ });
97
+ }
98
+ return cached.authInfo;
99
+ }
100
+ // Evict expired entry if present
101
+ if (cached) this.tokenCache.delete(cacheKey);
102
+
103
+ try {
104
+ const { payload } = await jwtVerify(token, this.jwks, {
105
+ issuer: this.options.issuer,
106
+ audience: this.options.audience,
107
+ });
108
+
109
+ const authInfo: AuthInfo = {
110
+ subject: payload.sub ?? "unknown",
111
+ clientId: (payload.azp as string | undefined) ??
112
+ (payload.client_id as string | undefined),
113
+ scopes: this.extractScopes(payload),
114
+ claims: payload as Record<string, unknown>,
115
+ expiresAt: payload.exp,
116
+ };
117
+
118
+ // Cache with TTL = min(token remaining lifetime, 5 minutes)
119
+ const tokenExpiresMs = payload.exp ? payload.exp * 1000 : Infinity;
120
+ const cacheTtl = Math.min(
121
+ tokenExpiresMs - Date.now(),
122
+ JwtAuthProvider.DEFAULT_CACHE_TTL_MS,
123
+ );
124
+ if (cacheTtl > 0) {
125
+ // Evict oldest entries if cache is full
126
+ if (this.tokenCache.size >= JwtAuthProvider.MAX_CACHE_SIZE) {
127
+ const oldestKey = this.tokenCache.keys().next().value;
128
+ if (oldestKey) this.tokenCache.delete(oldestKey);
129
+ }
130
+ this.tokenCache.set(cacheKey, {
131
+ authInfo: Object.freeze(authInfo) as AuthInfo,
132
+ expiresAt: Date.now() + cacheTtl,
133
+ });
134
+ }
135
+
136
+ return authInfo;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Hash a token for cache key (avoids storing raw tokens in memory)
144
+ */
145
+ private async hashToken(token: string): Promise<string> {
146
+ const data = new TextEncoder().encode(token);
147
+ const hash = await crypto.subtle.digest("SHA-256", data);
148
+ return Array.from(new Uint8Array(hash)).map((b) =>
149
+ b.toString(16).padStart(2, "0")
150
+ ).join("");
151
+ }
152
+
153
+ getResourceMetadata(): ProtectedResourceMetadata {
154
+ return {
155
+ resource: this.options.resource,
156
+ authorization_servers: this.options.authorizationServers,
157
+ scopes_supported: this.options.scopesSupported,
158
+ bearer_methods_supported: ["header"],
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Extract scopes from JWT payload.
164
+ * Supports: "scope" claim (space-separated string) and "scp" claim (array).
165
+ */
166
+ private extractScopes(payload: Record<string, unknown>): string[] {
167
+ if (typeof payload.scope === "string") {
168
+ return payload.scope.split(" ").filter(Boolean);
169
+ }
170
+ if (Array.isArray(payload.scp)) {
171
+ return payload.scp.filter((s): s is string => typeof s === "string");
172
+ }
173
+ return [];
174
+ }
175
+ }