mcp-ts-template 2.3.8 → 2.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +163 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-2.3.8-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--06--18-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.20.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg?style=flat-square)](https://github.com/cyanheads/mcp-ts-template/issues) [![TypeScript](https://img.shields.io/badge/TypeScript-^5.9.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.2.23-blueviolet.svg?style=flat-square)](https://bun.sh/) [![Code Coverage](https://img.shields.io/badge/Coverage-93.44%25-brightgreen.svg?style=flat-square)](./coverage/lcov-report/)
10
+ [![Version](https://img.shields.io/badge/Version-2.3.9-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--06--18-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.20.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg?style=flat-square)](https://github.com/cyanheads/mcp-ts-template/issues) [![TypeScript](https://img.shields.io/badge/TypeScript-^5.9.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.2.23-blueviolet.svg?style=flat-square)](https://bun.sh/) [![Code Coverage](https://img.shields.io/badge/Coverage-93.44%25-brightgreen.svg?style=flat-square)](./coverage/lcov-report/)
11
11
 
12
12
  </div>
13
13
 
package/dist/index.js CHANGED
@@ -117141,7 +117141,7 @@ var z = /* @__PURE__ */ Object.freeze({
117141
117141
  // package.json
117142
117142
  var package_default = {
117143
117143
  name: "mcp-ts-template",
117144
- version: "2.3.7",
117144
+ version: "2.3.8",
117145
117145
  mcpName: "io.github.cyanheads/mcp-ts-template",
117146
117146
  description: "The definitive, production-grade template for building powerful and scalable Model Context Protocol (MCP) servers with TypeScript, featuring built-in observability (OpenTelemetry), declarative tooling, robust error handling, and a modular, DI-driven architecture.",
117147
117147
  main: "dist/index.js",
@@ -119039,12 +119039,36 @@ class RateLimiter {
119039
119039
  maxRequests: 100,
119040
119040
  errorMessage: "Rate limit exceeded. Please try again in {waitTime} seconds.",
119041
119041
  skipInDevelopment: false,
119042
- cleanupInterval: 5 * 60 * 1000
119042
+ cleanupInterval: 5 * 60 * 1000,
119043
+ maxTrackedKeys: 1e4
119043
119044
  };
119044
119045
  this.effectiveConfig = { ...defaultConfig };
119045
119046
  this.limits = new Map;
119046
119047
  this.startCleanupTimer();
119047
119048
  }
119049
+ evictLRUEntry() {
119050
+ if (this.limits.size === 0)
119051
+ return;
119052
+ let oldestKey = null;
119053
+ let oldestTime = Infinity;
119054
+ for (const [key, entry] of this.limits.entries()) {
119055
+ if (entry.lastAccess < oldestTime) {
119056
+ oldestTime = entry.lastAccess;
119057
+ oldestKey = key;
119058
+ }
119059
+ }
119060
+ if (oldestKey) {
119061
+ this.limits.delete(oldestKey);
119062
+ const logContext = requestContextService.createRequestContext({
119063
+ operation: "RateLimiter.evictLRUEntry",
119064
+ additionalContext: {
119065
+ evictedKey: oldestKey,
119066
+ remainingEntries: this.limits.size
119067
+ }
119068
+ });
119069
+ this.logger.debug("Evicted LRU entry from rate limiter", logContext);
119070
+ }
119071
+ }
119048
119072
  startCleanupTimer() {
119049
119073
  if (this.cleanupTimer) {
119050
119074
  clearInterval(this.cleanupTimer);
@@ -119107,23 +119131,34 @@ class RateLimiter {
119107
119131
  const now = Date.now();
119108
119132
  let entry = this.limits.get(limitKey);
119109
119133
  if (!entry || now >= entry.resetTime) {
119134
+ const maxKeys = this.effectiveConfig.maxTrackedKeys || 1e4;
119135
+ if (!entry && this.limits.size >= maxKeys) {
119136
+ this.evictLRUEntry();
119137
+ activeSpan?.addEvent("rate_limit_lru_eviction", {
119138
+ "mcp.rate_limit.size_before_eviction": this.limits.size + 1,
119139
+ "mcp.rate_limit.max_keys": maxKeys
119140
+ });
119141
+ }
119110
119142
  entry = {
119111
119143
  count: 1,
119112
- resetTime: now + this.effectiveConfig.windowMs
119144
+ resetTime: now + this.effectiveConfig.windowMs,
119145
+ lastAccess: now
119113
119146
  };
119114
119147
  this.limits.set(limitKey, entry);
119115
119148
  } else {
119116
119149
  entry.count++;
119150
+ entry.lastAccess = now;
119117
119151
  }
119118
119152
  const remaining = Math.max(0, this.effectiveConfig.maxRequests - entry.count);
119119
119153
  activeSpan?.setAttributes({
119120
119154
  "mcp.rate_limit.limit": this.effectiveConfig.maxRequests,
119121
119155
  "mcp.rate_limit.count": entry.count,
119122
- "mcp.rate_limit.remaining": remaining
119156
+ "mcp.rate_limit.remaining": remaining,
119157
+ "mcp.rate_limit.tracked_keys": this.limits.size
119123
119158
  });
119124
119159
  if (entry.count > this.effectiveConfig.maxRequests) {
119125
119160
  const waitTime = Math.ceil((entry.resetTime - now) / 1000);
119126
- const errorMessage = (this.effectiveConfig.errorMessage || "Rate limit exceeded. Please try again in {waitTime} seconds.").replace("{waitTime}", waitTime.toString());
119161
+ const errorMessage = (this.effectiveConfig.errorMessage || "Rate limit exceeded. Please try again in {waitTime} seconds.").replace("{waitTime}", waitTime.toString());
119127
119162
  activeSpan?.addEvent("rate_limit_exceeded", {
119128
119163
  "mcp.rate_limit.wait_time_seconds": waitTime
119129
119164
  });
@@ -125604,13 +125639,51 @@ class SpeechService2 {
125604
125639
  // src/storage/core/StorageService.ts
125605
125640
  var import_tsyringe5 = __toESM(require_cjs3(), 1);
125606
125641
  function requireTenantId(context) {
125607
- if (!context.tenantId) {
125642
+ const tenantId = context.tenantId;
125643
+ if (tenantId === undefined || tenantId === null) {
125608
125644
  throw new McpError(-32603 /* InternalError */, "Tenant ID is required for storage operations but was not found in the request context.", {
125609
125645
  operation: "StorageService.requireTenantId",
125610
125646
  requestId: context.requestId
125611
125647
  });
125612
125648
  }
125613
- return context.tenantId;
125649
+ if (typeof tenantId !== "string" || tenantId.trim().length === 0) {
125650
+ throw new McpError(-32602 /* InvalidParams */, "Tenant ID cannot be an empty string.", {
125651
+ operation: "StorageService.requireTenantId",
125652
+ requestId: context.requestId,
125653
+ tenantId
125654
+ });
125655
+ }
125656
+ const trimmedTenantId = tenantId.trim();
125657
+ if (trimmedTenantId.length > 128) {
125658
+ throw new McpError(-32602 /* InvalidParams */, "Tenant ID exceeds maximum length of 128 characters.", {
125659
+ operation: "StorageService.requireTenantId",
125660
+ requestId: context.requestId,
125661
+ tenantIdLength: trimmedTenantId.length
125662
+ });
125663
+ }
125664
+ const validTenantIdPattern = /^[a-zA-Z0-9]$|^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$/;
125665
+ if (!validTenantIdPattern.test(trimmedTenantId)) {
125666
+ throw new McpError(-32602 /* InvalidParams */, "Tenant ID contains invalid characters. Only alphanumeric characters, hyphens, underscores, and dots are allowed. Must start and end with alphanumeric characters.", {
125667
+ operation: "StorageService.requireTenantId",
125668
+ requestId: context.requestId,
125669
+ tenantId: trimmedTenantId
125670
+ });
125671
+ }
125672
+ if (trimmedTenantId.includes("../") || trimmedTenantId.includes("..\\")) {
125673
+ throw new McpError(-32602 /* InvalidParams */, "Tenant ID contains path traversal sequences, which are not allowed.", {
125674
+ operation: "StorageService.requireTenantId",
125675
+ requestId: context.requestId,
125676
+ tenantId: trimmedTenantId
125677
+ });
125678
+ }
125679
+ if (trimmedTenantId.includes("..")) {
125680
+ throw new McpError(-32602 /* InvalidParams */, "Tenant ID contains consecutive dots, which are not allowed.", {
125681
+ operation: "StorageService.requireTenantId",
125682
+ requestId: context.requestId,
125683
+ tenantId: trimmedTenantId
125684
+ });
125685
+ }
125686
+ return trimmedTenantId;
125614
125687
  }
125615
125688
 
125616
125689
  class StorageService2 {
@@ -135451,26 +135524,48 @@ class SessionStore {
135451
135524
  this.staleTimeout = staleTimeoutMs;
135452
135525
  setInterval(() => this.cleanupStaleSessions(), 60000);
135453
135526
  }
135454
- getOrCreate(sessionId) {
135527
+ getOrCreate(sessionId, identity) {
135455
135528
  let session = this.sessions.get(sessionId);
135456
135529
  if (!session) {
135457
- session = {
135530
+ const newSession = {
135458
135531
  id: sessionId,
135459
135532
  createdAt: new Date,
135460
135533
  lastAccessedAt: new Date
135461
135534
  };
135535
+ if (identity?.tenantId)
135536
+ newSession.tenantId = identity.tenantId;
135537
+ if (identity?.clientId)
135538
+ newSession.clientId = identity.clientId;
135539
+ if (identity?.subject)
135540
+ newSession.subject = identity.subject;
135541
+ session = newSession;
135462
135542
  this.sessions.set(sessionId, session);
135463
135543
  const context = requestContextService.createRequestContext({
135464
135544
  operation: "SessionStore.create",
135465
- sessionId
135545
+ sessionId,
135546
+ tenantId: identity?.tenantId
135466
135547
  });
135467
- logger.debug("Session created", context);
135548
+ logger.debug("Session created with identity binding", context);
135468
135549
  } else {
135469
135550
  session.lastAccessedAt = new Date;
135551
+ if (identity && !session.tenantId) {
135552
+ if (identity.tenantId)
135553
+ session.tenantId = identity.tenantId;
135554
+ if (identity.clientId)
135555
+ session.clientId = identity.clientId;
135556
+ if (identity.subject)
135557
+ session.subject = identity.subject;
135558
+ const context = requestContextService.createRequestContext({
135559
+ operation: "SessionStore.bindIdentity",
135560
+ sessionId,
135561
+ tenantId: identity.tenantId
135562
+ });
135563
+ logger.debug("Session identity bound on authenticated request", context);
135564
+ }
135470
135565
  }
135471
135566
  return session;
135472
135567
  }
135473
- isValid(sessionId) {
135568
+ isValidForIdentity(sessionId, identity) {
135474
135569
  const session = this.sessions.get(sessionId);
135475
135570
  if (!session) {
135476
135571
  return false;
@@ -135480,6 +135575,45 @@ class SessionStore {
135480
135575
  this.terminate(sessionId);
135481
135576
  return false;
135482
135577
  }
135578
+ if (!session.tenantId && !session.clientId) {
135579
+ return true;
135580
+ }
135581
+ if (!identity) {
135582
+ const context = requestContextService.createRequestContext({
135583
+ operation: "SessionStore.isValidForIdentity",
135584
+ sessionId
135585
+ });
135586
+ logger.warning("Session requires authentication but request has no identity", context);
135587
+ return false;
135588
+ }
135589
+ if (session.tenantId && identity.tenantId) {
135590
+ if (session.tenantId !== identity.tenantId) {
135591
+ const context = requestContextService.createRequestContext({
135592
+ operation: "SessionStore.isValidForIdentity",
135593
+ sessionId
135594
+ });
135595
+ logger.warning("Session tenant mismatch - possible hijacking attempt", {
135596
+ ...context,
135597
+ sessionTenant: session.tenantId,
135598
+ requestTenant: identity.tenantId
135599
+ });
135600
+ return false;
135601
+ }
135602
+ }
135603
+ if (session.clientId && identity.clientId) {
135604
+ if (session.clientId !== identity.clientId) {
135605
+ const context = requestContextService.createRequestContext({
135606
+ operation: "SessionStore.isValidForIdentity",
135607
+ sessionId
135608
+ });
135609
+ logger.warning("Session client mismatch - possible hijacking attempt", {
135610
+ ...context,
135611
+ sessionClient: session.clientId,
135612
+ requestClient: identity.clientId
135613
+ });
135614
+ return false;
135615
+ }
135616
+ }
135483
135617
  return true;
135484
135618
  }
135485
135619
  terminate(sessionId) {
@@ -135637,15 +135771,28 @@ function createHttpApp(mcpServer, parentContext) {
135637
135771
  }
135638
135772
  const providedSessionId = c.req.header("mcp-session-id");
135639
135773
  const sessionId = providedSessionId ?? randomUUID();
135640
- if (sessionStore && providedSessionId && !sessionStore.isValid(providedSessionId)) {
135641
- logger.warning("Request with invalid or terminated session ID", {
135774
+ const authStore = authContext.getStore();
135775
+ let sessionIdentity;
135776
+ if (authStore?.authInfo) {
135777
+ sessionIdentity = {};
135778
+ if (authStore.authInfo.tenantId)
135779
+ sessionIdentity.tenantId = authStore.authInfo.tenantId;
135780
+ if (authStore.authInfo.clientId)
135781
+ sessionIdentity.clientId = authStore.authInfo.clientId;
135782
+ if (authStore.authInfo.subject)
135783
+ sessionIdentity.subject = authStore.authInfo.subject;
135784
+ }
135785
+ if (sessionStore && providedSessionId && !sessionStore.isValidForIdentity(providedSessionId, sessionIdentity)) {
135786
+ logger.warning("Session validation failed - invalid or hijacked session", {
135642
135787
  ...transportContext,
135643
- sessionId: providedSessionId
135788
+ sessionId: providedSessionId,
135789
+ requestTenant: sessionIdentity?.tenantId,
135790
+ requestClient: sessionIdentity?.clientId
135644
135791
  });
135645
135792
  return c.json({ error: "Session not found or expired" }, 404);
135646
135793
  }
135647
135794
  if (sessionStore) {
135648
- sessionStore.getOrCreate(sessionId);
135795
+ sessionStore.getOrCreate(sessionId, sessionIdentity);
135649
135796
  }
135650
135797
  const transport = new McpSessionTransport(sessionId);
135651
135798
  const handleRpc = async () => {
@@ -136047,7 +136194,6 @@ var start = async () => {
136047
136194
  }
136048
136195
  }
136049
136196
  await logger.initialize(validatedMcpLogLevel);
136050
- logger.info(`Logger initialized. Effective MCP logging level: ${validatedMcpLogLevel}.`, requestContextService.createRequestContext({ operation: "LoggerInit" }));
136051
136197
  logger.info(`Storage service initialized with provider: ${config2.storage.providerType}`, requestContextService.createRequestContext({ operation: "StorageInit" }));
136052
136198
  transportManager = container_default.resolve(TransportManagerToken);
136053
136199
  const startupContext = requestContextService.createRequestContext({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ts-template",
3
- "version": "2.3.8",
3
+ "version": "2.3.9",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-template",
5
5
  "description": "The definitive, production-grade template for building powerful and scalable Model Context Protocol (MCP) servers with TypeScript, featuring built-in observability (OpenTelemetry), declarative tooling, robust error handling, and a modular, DI-driven architecture.",
6
6
  "main": "dist/index.js",