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.
- package/README.md +1 -1
- package/dist/index.js +163 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx) [](https://modelcontextprotocol.io/) [](./LICENSE) [](https://github.com/cyanheads/mcp-ts-template/issues) [](https://www.typescriptlang.org/) [](https://bun.sh/) [](./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.
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135641
|
-
|
|
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.
|
|
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",
|