mcp-ts-template 2.3.7 → 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 +174 -26
- 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 {
|
|
@@ -126735,15 +126808,17 @@ var echoResourceDefinition = {
|
|
|
126735
126808
|
mimeType: "application/json",
|
|
126736
126809
|
examples: [{ name: "Basic echo", uri: "echo://hello" }],
|
|
126737
126810
|
annotations: { readOnlyHint: true },
|
|
126738
|
-
list: () =>
|
|
126739
|
-
|
|
126740
|
-
|
|
126741
|
-
|
|
126742
|
-
|
|
126743
|
-
|
|
126744
|
-
|
|
126745
|
-
|
|
126746
|
-
|
|
126811
|
+
list: (_extra) => {
|
|
126812
|
+
return {
|
|
126813
|
+
resources: [
|
|
126814
|
+
{
|
|
126815
|
+
uri: "echo://hello",
|
|
126816
|
+
name: "Default Echo Message",
|
|
126817
|
+
description: "A simple echo resource example."
|
|
126818
|
+
}
|
|
126819
|
+
]
|
|
126820
|
+
};
|
|
126821
|
+
},
|
|
126747
126822
|
logic: withResourceAuth(["resource:echo:read"], echoLogic)
|
|
126748
126823
|
};
|
|
126749
126824
|
|
|
@@ -135449,26 +135524,48 @@ class SessionStore {
|
|
|
135449
135524
|
this.staleTimeout = staleTimeoutMs;
|
|
135450
135525
|
setInterval(() => this.cleanupStaleSessions(), 60000);
|
|
135451
135526
|
}
|
|
135452
|
-
getOrCreate(sessionId) {
|
|
135527
|
+
getOrCreate(sessionId, identity) {
|
|
135453
135528
|
let session = this.sessions.get(sessionId);
|
|
135454
135529
|
if (!session) {
|
|
135455
|
-
|
|
135530
|
+
const newSession = {
|
|
135456
135531
|
id: sessionId,
|
|
135457
135532
|
createdAt: new Date,
|
|
135458
135533
|
lastAccessedAt: new Date
|
|
135459
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;
|
|
135460
135542
|
this.sessions.set(sessionId, session);
|
|
135461
135543
|
const context = requestContextService.createRequestContext({
|
|
135462
135544
|
operation: "SessionStore.create",
|
|
135463
|
-
sessionId
|
|
135545
|
+
sessionId,
|
|
135546
|
+
tenantId: identity?.tenantId
|
|
135464
135547
|
});
|
|
135465
|
-
logger.debug("Session created", context);
|
|
135548
|
+
logger.debug("Session created with identity binding", context);
|
|
135466
135549
|
} else {
|
|
135467
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
|
+
}
|
|
135468
135565
|
}
|
|
135469
135566
|
return session;
|
|
135470
135567
|
}
|
|
135471
|
-
|
|
135568
|
+
isValidForIdentity(sessionId, identity) {
|
|
135472
135569
|
const session = this.sessions.get(sessionId);
|
|
135473
135570
|
if (!session) {
|
|
135474
135571
|
return false;
|
|
@@ -135478,6 +135575,45 @@ class SessionStore {
|
|
|
135478
135575
|
this.terminate(sessionId);
|
|
135479
135576
|
return false;
|
|
135480
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
|
+
}
|
|
135481
135617
|
return true;
|
|
135482
135618
|
}
|
|
135483
135619
|
terminate(sessionId) {
|
|
@@ -135635,15 +135771,28 @@ function createHttpApp(mcpServer, parentContext) {
|
|
|
135635
135771
|
}
|
|
135636
135772
|
const providedSessionId = c.req.header("mcp-session-id");
|
|
135637
135773
|
const sessionId = providedSessionId ?? randomUUID();
|
|
135638
|
-
|
|
135639
|
-
|
|
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", {
|
|
135640
135787
|
...transportContext,
|
|
135641
|
-
sessionId: providedSessionId
|
|
135788
|
+
sessionId: providedSessionId,
|
|
135789
|
+
requestTenant: sessionIdentity?.tenantId,
|
|
135790
|
+
requestClient: sessionIdentity?.clientId
|
|
135642
135791
|
});
|
|
135643
135792
|
return c.json({ error: "Session not found or expired" }, 404);
|
|
135644
135793
|
}
|
|
135645
135794
|
if (sessionStore) {
|
|
135646
|
-
sessionStore.getOrCreate(sessionId);
|
|
135795
|
+
sessionStore.getOrCreate(sessionId, sessionIdentity);
|
|
135647
135796
|
}
|
|
135648
135797
|
const transport = new McpSessionTransport(sessionId);
|
|
135649
135798
|
const handleRpc = async () => {
|
|
@@ -136045,7 +136194,6 @@ var start = async () => {
|
|
|
136045
136194
|
}
|
|
136046
136195
|
}
|
|
136047
136196
|
await logger.initialize(validatedMcpLogLevel);
|
|
136048
|
-
logger.info(`Logger initialized. Effective MCP logging level: ${validatedMcpLogLevel}.`, requestContextService.createRequestContext({ operation: "LoggerInit" }));
|
|
136049
136197
|
logger.info(`Storage service initialized with provider: ${config2.storage.providerType}`, requestContextService.createRequestContext({ operation: "StorageInit" }));
|
|
136050
136198
|
transportManager = container_default.resolve(TransportManagerToken);
|
|
136051
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",
|