fastmcp 3.18.0 → 3.19.1
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 +3 -2
- package/dist/FastMCP.js +24 -29
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/FastMCP.test.ts +585 -0
- package/src/FastMCP.ts +28 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastmcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.1",
|
|
4
4
|
"main": "dist/FastMCP.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsup",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"execa": "^9.6.0",
|
|
28
28
|
"file-type": "^21.0.0",
|
|
29
29
|
"fuse.js": "^7.1.0",
|
|
30
|
-
"mcp-proxy": "^5.
|
|
30
|
+
"mcp-proxy": "^5.8.0",
|
|
31
31
|
"strict-event-emitter-types": "^2.0.0",
|
|
32
32
|
"undici": "^7.13.0",
|
|
33
33
|
"uri-templates": "^0.2.0",
|
package/src/FastMCP.test.ts
CHANGED
|
@@ -1052,6 +1052,238 @@ test("embedded resources work with direct resources", async () => {
|
|
|
1052
1052
|
});
|
|
1053
1053
|
});
|
|
1054
1054
|
|
|
1055
|
+
test("embedded resources work with URI templates and query parameters", async () => {
|
|
1056
|
+
await runWithTestServer({
|
|
1057
|
+
run: async ({ client }) => {
|
|
1058
|
+
// Test case 1: Simple query parameter extraction
|
|
1059
|
+
expect(
|
|
1060
|
+
await client.callTool({
|
|
1061
|
+
arguments: {
|
|
1062
|
+
uri: "ui://search?location=a&q=b",
|
|
1063
|
+
},
|
|
1064
|
+
name: "get_search_resource",
|
|
1065
|
+
}),
|
|
1066
|
+
).toEqual({
|
|
1067
|
+
content: [
|
|
1068
|
+
{
|
|
1069
|
+
resource: {
|
|
1070
|
+
mimeType: "application/json",
|
|
1071
|
+
text: '{"location":"a","query":"b","type":"search"}',
|
|
1072
|
+
uri: "ui://search?location=a&q=b",
|
|
1073
|
+
},
|
|
1074
|
+
type: "resource",
|
|
1075
|
+
},
|
|
1076
|
+
],
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// Test case 2: Query parameters with different order
|
|
1080
|
+
expect(
|
|
1081
|
+
await client.callTool({
|
|
1082
|
+
arguments: {
|
|
1083
|
+
uri: "ui://search?q=test&location=home",
|
|
1084
|
+
},
|
|
1085
|
+
name: "get_search_resource",
|
|
1086
|
+
}),
|
|
1087
|
+
).toEqual({
|
|
1088
|
+
content: [
|
|
1089
|
+
{
|
|
1090
|
+
resource: {
|
|
1091
|
+
mimeType: "application/json",
|
|
1092
|
+
text: '{"location":"home","query":"test","type":"search"}',
|
|
1093
|
+
uri: "ui://search?q=test&location=home",
|
|
1094
|
+
},
|
|
1095
|
+
type: "resource",
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Test case 3: Query parameters with encoded values
|
|
1101
|
+
expect(
|
|
1102
|
+
await client.callTool({
|
|
1103
|
+
arguments: {
|
|
1104
|
+
uri: "ui://search?location=new%20york&q=hello%20world",
|
|
1105
|
+
},
|
|
1106
|
+
name: "get_search_resource",
|
|
1107
|
+
}),
|
|
1108
|
+
).toEqual({
|
|
1109
|
+
content: [
|
|
1110
|
+
{
|
|
1111
|
+
resource: {
|
|
1112
|
+
mimeType: "application/json",
|
|
1113
|
+
text: '{"location":"new york","query":"hello world","type":"search"}',
|
|
1114
|
+
uri: "ui://search?location=new%20york&q=hello%20world",
|
|
1115
|
+
},
|
|
1116
|
+
type: "resource",
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
});
|
|
1120
|
+
},
|
|
1121
|
+
|
|
1122
|
+
server: async () => {
|
|
1123
|
+
const server = new FastMCP({
|
|
1124
|
+
name: "Test",
|
|
1125
|
+
version: "1.0.0",
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
server.addResourceTemplate({
|
|
1129
|
+
arguments: [
|
|
1130
|
+
{
|
|
1131
|
+
name: "location",
|
|
1132
|
+
required: true,
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
name: "q",
|
|
1136
|
+
required: true,
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
async load(args) {
|
|
1140
|
+
return {
|
|
1141
|
+
text: JSON.stringify({
|
|
1142
|
+
location: args.location,
|
|
1143
|
+
query: args.q,
|
|
1144
|
+
type: "search",
|
|
1145
|
+
}),
|
|
1146
|
+
};
|
|
1147
|
+
},
|
|
1148
|
+
mimeType: "application/json",
|
|
1149
|
+
name: "Search Resource",
|
|
1150
|
+
uriTemplate: "ui://search{?location,q}",
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
server.addTool({
|
|
1154
|
+
description:
|
|
1155
|
+
"Get search resource data using embedded function with query parameters",
|
|
1156
|
+
execute: async (args) => {
|
|
1157
|
+
return {
|
|
1158
|
+
content: [
|
|
1159
|
+
{
|
|
1160
|
+
resource: await server.embedded(args.uri),
|
|
1161
|
+
type: "resource",
|
|
1162
|
+
},
|
|
1163
|
+
],
|
|
1164
|
+
};
|
|
1165
|
+
},
|
|
1166
|
+
name: "get_search_resource",
|
|
1167
|
+
parameters: z.object({
|
|
1168
|
+
uri: z.string(),
|
|
1169
|
+
}),
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
return server;
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("embedded resources work with complex URI template patterns", async () => {
|
|
1178
|
+
await runWithTestServer({
|
|
1179
|
+
run: async ({ client }) => {
|
|
1180
|
+
// Test case 1: Path and query parameters combined
|
|
1181
|
+
expect(
|
|
1182
|
+
await client.callTool({
|
|
1183
|
+
arguments: {
|
|
1184
|
+
uri: "api://users/123?fields=name,email&format=json",
|
|
1185
|
+
},
|
|
1186
|
+
name: "get_user_data",
|
|
1187
|
+
}),
|
|
1188
|
+
).toEqual({
|
|
1189
|
+
content: [
|
|
1190
|
+
{
|
|
1191
|
+
resource: {
|
|
1192
|
+
mimeType: "application/json",
|
|
1193
|
+
text: '{"userId":"123","fields":["name","email"],"format":"json"}',
|
|
1194
|
+
uri: "api://users/123?fields=name,email&format=json",
|
|
1195
|
+
},
|
|
1196
|
+
type: "resource",
|
|
1197
|
+
},
|
|
1198
|
+
],
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
// Test case 2: Optional query parameters (some missing)
|
|
1202
|
+
expect(
|
|
1203
|
+
await client.callTool({
|
|
1204
|
+
arguments: {
|
|
1205
|
+
uri: "api://users/456?format=xml",
|
|
1206
|
+
},
|
|
1207
|
+
name: "get_user_data",
|
|
1208
|
+
}),
|
|
1209
|
+
).toEqual({
|
|
1210
|
+
content: [
|
|
1211
|
+
{
|
|
1212
|
+
resource: {
|
|
1213
|
+
mimeType: "application/json",
|
|
1214
|
+
text: '{"userId":"456","format":"xml"}',
|
|
1215
|
+
uri: "api://users/456?format=xml",
|
|
1216
|
+
},
|
|
1217
|
+
type: "resource",
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
});
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
server: async () => {
|
|
1224
|
+
const server = new FastMCP({
|
|
1225
|
+
name: "Test",
|
|
1226
|
+
version: "1.0.0",
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
server.addResourceTemplate({
|
|
1230
|
+
arguments: [
|
|
1231
|
+
{
|
|
1232
|
+
name: "userId",
|
|
1233
|
+
required: true,
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
name: "fields",
|
|
1237
|
+
required: false,
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
name: "format",
|
|
1241
|
+
required: false,
|
|
1242
|
+
},
|
|
1243
|
+
],
|
|
1244
|
+
async load(args) {
|
|
1245
|
+
const result: Record<string, string> = {
|
|
1246
|
+
userId: args.userId,
|
|
1247
|
+
};
|
|
1248
|
+
if (args.fields) {
|
|
1249
|
+
result.fields = args.fields;
|
|
1250
|
+
}
|
|
1251
|
+
if (args.format) {
|
|
1252
|
+
result.format = args.format;
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
text: JSON.stringify(result),
|
|
1256
|
+
};
|
|
1257
|
+
},
|
|
1258
|
+
mimeType: "application/json",
|
|
1259
|
+
name: "User Data API",
|
|
1260
|
+
uriTemplate: "api://users/{userId}{?fields,format}",
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
server.addTool({
|
|
1264
|
+
description:
|
|
1265
|
+
"Get user data using complex URI templates with path and query parameters",
|
|
1266
|
+
execute: async (args) => {
|
|
1267
|
+
return {
|
|
1268
|
+
content: [
|
|
1269
|
+
{
|
|
1270
|
+
resource: await server.embedded(args.uri),
|
|
1271
|
+
type: "resource",
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1274
|
+
};
|
|
1275
|
+
},
|
|
1276
|
+
name: "get_user_data",
|
|
1277
|
+
parameters: z.object({
|
|
1278
|
+
uri: z.string(),
|
|
1279
|
+
}),
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
return server;
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1055
1287
|
test("adds prompts", async () => {
|
|
1056
1288
|
await runWithTestServer({
|
|
1057
1289
|
run: async ({ client }) => {
|
|
@@ -3293,6 +3525,359 @@ test("stateless mode health check includes mode indicator", async () => {
|
|
|
3293
3525
|
}
|
|
3294
3526
|
});
|
|
3295
3527
|
|
|
3528
|
+
test("stateless mode with valid authentication allows access", async () => {
|
|
3529
|
+
const port = await getRandomPort();
|
|
3530
|
+
|
|
3531
|
+
const server = new FastMCP<{ userId: string }>({
|
|
3532
|
+
authenticate: async () => {
|
|
3533
|
+
// Always authenticate successfully for this test
|
|
3534
|
+
return { userId: "123" };
|
|
3535
|
+
},
|
|
3536
|
+
name: "Test server",
|
|
3537
|
+
version: "1.0.0",
|
|
3538
|
+
});
|
|
3539
|
+
|
|
3540
|
+
server.addTool({
|
|
3541
|
+
description: "Test tool",
|
|
3542
|
+
execute: async () => {
|
|
3543
|
+
return "pong";
|
|
3544
|
+
},
|
|
3545
|
+
name: "ping",
|
|
3546
|
+
parameters: z.object({}),
|
|
3547
|
+
});
|
|
3548
|
+
|
|
3549
|
+
await server.start({
|
|
3550
|
+
httpStream: {
|
|
3551
|
+
port,
|
|
3552
|
+
stateless: true,
|
|
3553
|
+
},
|
|
3554
|
+
transportType: "httpStream",
|
|
3555
|
+
});
|
|
3556
|
+
|
|
3557
|
+
try {
|
|
3558
|
+
const client = new Client(
|
|
3559
|
+
{
|
|
3560
|
+
name: "Test client",
|
|
3561
|
+
version: "1.0.0",
|
|
3562
|
+
},
|
|
3563
|
+
{
|
|
3564
|
+
capabilities: {},
|
|
3565
|
+
},
|
|
3566
|
+
);
|
|
3567
|
+
|
|
3568
|
+
const transport = new StreamableHTTPClientTransport(
|
|
3569
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
3570
|
+
);
|
|
3571
|
+
|
|
3572
|
+
await client.connect(transport);
|
|
3573
|
+
|
|
3574
|
+
const result = await client.callTool({
|
|
3575
|
+
arguments: {},
|
|
3576
|
+
name: "ping",
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
expect(result.content).toEqual([
|
|
3580
|
+
{
|
|
3581
|
+
text: "pong",
|
|
3582
|
+
type: "text",
|
|
3583
|
+
},
|
|
3584
|
+
]);
|
|
3585
|
+
|
|
3586
|
+
// Server should not track sessions in stateless mode
|
|
3587
|
+
expect(server.sessions.length).toBe(0);
|
|
3588
|
+
|
|
3589
|
+
await client.close();
|
|
3590
|
+
} finally {
|
|
3591
|
+
await server.stop();
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3595
|
+
test("stateless mode rejects missing Authorization header", async () => {
|
|
3596
|
+
const port = await getRandomPort();
|
|
3597
|
+
|
|
3598
|
+
const server = new FastMCP<{ userId: string }>({
|
|
3599
|
+
authenticate: async (req) => {
|
|
3600
|
+
const authHeader = req.headers.authorization;
|
|
3601
|
+
|
|
3602
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3603
|
+
throw new Response(null, {
|
|
3604
|
+
status: 401,
|
|
3605
|
+
statusText: "Unauthorized",
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
return { userId: "123" };
|
|
3610
|
+
},
|
|
3611
|
+
name: "Test server",
|
|
3612
|
+
version: "1.0.0",
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
server.addTool({
|
|
3616
|
+
description: "Test tool",
|
|
3617
|
+
execute: async () => {
|
|
3618
|
+
return "pong";
|
|
3619
|
+
},
|
|
3620
|
+
name: "ping",
|
|
3621
|
+
parameters: z.object({}),
|
|
3622
|
+
});
|
|
3623
|
+
|
|
3624
|
+
await server.start({
|
|
3625
|
+
httpStream: {
|
|
3626
|
+
port,
|
|
3627
|
+
stateless: true,
|
|
3628
|
+
},
|
|
3629
|
+
transportType: "httpStream",
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
try {
|
|
3633
|
+
// Send a raw HTTP request without Authorization header
|
|
3634
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
3635
|
+
body: JSON.stringify({
|
|
3636
|
+
id: 1,
|
|
3637
|
+
jsonrpc: "2.0",
|
|
3638
|
+
method: "tools/call",
|
|
3639
|
+
params: {
|
|
3640
|
+
arguments: {},
|
|
3641
|
+
name: "ping",
|
|
3642
|
+
},
|
|
3643
|
+
}),
|
|
3644
|
+
headers: {
|
|
3645
|
+
"Content-Type": "application/json",
|
|
3646
|
+
},
|
|
3647
|
+
method: "POST",
|
|
3648
|
+
});
|
|
3649
|
+
|
|
3650
|
+
expect(response.status).toBe(401);
|
|
3651
|
+
|
|
3652
|
+
const body = (await response.json()) as { error?: { message?: string } };
|
|
3653
|
+
expect(body.error?.message).toContain("Unauthorized");
|
|
3654
|
+
} finally {
|
|
3655
|
+
await server.stop();
|
|
3656
|
+
}
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
test("stateless mode rejects invalid authentication token", async () => {
|
|
3660
|
+
const port = await getRandomPort();
|
|
3661
|
+
const VALID_TOKEN = "valid_jwt_token";
|
|
3662
|
+
const INVALID_TOKEN = "invalid_jwt_token";
|
|
3663
|
+
|
|
3664
|
+
const server = new FastMCP<{ userId: string }>({
|
|
3665
|
+
authenticate: async (req) => {
|
|
3666
|
+
const authHeader = req.headers.authorization;
|
|
3667
|
+
|
|
3668
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3669
|
+
throw new Response(null, {
|
|
3670
|
+
status: 401,
|
|
3671
|
+
statusText: "Unauthorized",
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
const token = authHeader.split(" ")[1];
|
|
3676
|
+
|
|
3677
|
+
if (token === VALID_TOKEN) {
|
|
3678
|
+
return { userId: "123" };
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
throw new Response(null, {
|
|
3682
|
+
status: 401,
|
|
3683
|
+
statusText: "Unauthorized",
|
|
3684
|
+
});
|
|
3685
|
+
},
|
|
3686
|
+
name: "Test server",
|
|
3687
|
+
version: "1.0.0",
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
server.addTool({
|
|
3691
|
+
description: "Test tool",
|
|
3692
|
+
execute: async () => {
|
|
3693
|
+
return "pong";
|
|
3694
|
+
},
|
|
3695
|
+
name: "ping",
|
|
3696
|
+
parameters: z.object({}),
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3699
|
+
await server.start({
|
|
3700
|
+
httpStream: {
|
|
3701
|
+
port,
|
|
3702
|
+
stateless: true,
|
|
3703
|
+
},
|
|
3704
|
+
transportType: "httpStream",
|
|
3705
|
+
});
|
|
3706
|
+
|
|
3707
|
+
try {
|
|
3708
|
+
// Send a raw HTTP request with invalid token
|
|
3709
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
3710
|
+
body: JSON.stringify({
|
|
3711
|
+
id: 1,
|
|
3712
|
+
jsonrpc: "2.0",
|
|
3713
|
+
method: "tools/call",
|
|
3714
|
+
params: {
|
|
3715
|
+
arguments: {},
|
|
3716
|
+
name: "ping",
|
|
3717
|
+
},
|
|
3718
|
+
}),
|
|
3719
|
+
headers: {
|
|
3720
|
+
Authorization: `Bearer ${INVALID_TOKEN}`,
|
|
3721
|
+
"Content-Type": "application/json",
|
|
3722
|
+
},
|
|
3723
|
+
method: "POST",
|
|
3724
|
+
});
|
|
3725
|
+
|
|
3726
|
+
expect(response.status).toBe(401);
|
|
3727
|
+
|
|
3728
|
+
const body = (await response.json()) as { error?: { message?: string } };
|
|
3729
|
+
expect(body.error?.message).toContain("Unauthorized");
|
|
3730
|
+
} finally {
|
|
3731
|
+
await server.stop();
|
|
3732
|
+
}
|
|
3733
|
+
});
|
|
3734
|
+
|
|
3735
|
+
test("stateless mode handles authentication function throwing errors", async () => {
|
|
3736
|
+
const port = await getRandomPort();
|
|
3737
|
+
|
|
3738
|
+
const server = new FastMCP<{ userId: string }>({
|
|
3739
|
+
authenticate: async () => {
|
|
3740
|
+
// Simulate an internal error during token validation
|
|
3741
|
+
throw new Error("JWT validation service is down");
|
|
3742
|
+
},
|
|
3743
|
+
name: "Test server",
|
|
3744
|
+
version: "1.0.0",
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
server.addTool({
|
|
3748
|
+
description: "Test tool",
|
|
3749
|
+
execute: async () => {
|
|
3750
|
+
return "pong";
|
|
3751
|
+
},
|
|
3752
|
+
name: "ping",
|
|
3753
|
+
parameters: z.object({}),
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
await server.start({
|
|
3757
|
+
httpStream: {
|
|
3758
|
+
port,
|
|
3759
|
+
stateless: true,
|
|
3760
|
+
},
|
|
3761
|
+
transportType: "httpStream",
|
|
3762
|
+
});
|
|
3763
|
+
|
|
3764
|
+
try {
|
|
3765
|
+
// Send a raw HTTP request
|
|
3766
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
3767
|
+
body: JSON.stringify({
|
|
3768
|
+
id: 1,
|
|
3769
|
+
jsonrpc: "2.0",
|
|
3770
|
+
method: "tools/call",
|
|
3771
|
+
params: {
|
|
3772
|
+
arguments: {},
|
|
3773
|
+
name: "ping",
|
|
3774
|
+
},
|
|
3775
|
+
}),
|
|
3776
|
+
headers: {
|
|
3777
|
+
Authorization: "Bearer any_token",
|
|
3778
|
+
"Content-Type": "application/json",
|
|
3779
|
+
},
|
|
3780
|
+
method: "POST",
|
|
3781
|
+
});
|
|
3782
|
+
|
|
3783
|
+
expect(response.status).toBe(401);
|
|
3784
|
+
|
|
3785
|
+
const body = (await response.json()) as { error?: { message?: string } };
|
|
3786
|
+
expect(body.error?.message).toContain("Unauthorized");
|
|
3787
|
+
} finally {
|
|
3788
|
+
await server.stop();
|
|
3789
|
+
}
|
|
3790
|
+
});
|
|
3791
|
+
|
|
3792
|
+
test("stateless mode handles concurrent requests with authentication", async () => {
|
|
3793
|
+
const port = await getRandomPort();
|
|
3794
|
+
let requestCount = 0;
|
|
3795
|
+
|
|
3796
|
+
const server = new FastMCP<{ requestId: number }>({
|
|
3797
|
+
authenticate: async () => {
|
|
3798
|
+
// Track each authentication request
|
|
3799
|
+
requestCount++;
|
|
3800
|
+
return { requestId: requestCount };
|
|
3801
|
+
},
|
|
3802
|
+
name: "Test server",
|
|
3803
|
+
version: "1.0.0",
|
|
3804
|
+
});
|
|
3805
|
+
|
|
3806
|
+
server.addTool({
|
|
3807
|
+
description: "Echo request ID",
|
|
3808
|
+
execute: async (_args, context) => {
|
|
3809
|
+
return `Request ${context.session?.requestId}`;
|
|
3810
|
+
},
|
|
3811
|
+
name: "whoami",
|
|
3812
|
+
parameters: z.object({}),
|
|
3813
|
+
});
|
|
3814
|
+
|
|
3815
|
+
await server.start({
|
|
3816
|
+
httpStream: {
|
|
3817
|
+
port,
|
|
3818
|
+
stateless: true,
|
|
3819
|
+
},
|
|
3820
|
+
transportType: "httpStream",
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
try {
|
|
3824
|
+
// Create two clients to test concurrent stateless requests
|
|
3825
|
+
const client1 = new Client(
|
|
3826
|
+
{
|
|
3827
|
+
name: "Client 1",
|
|
3828
|
+
version: "1.0.0",
|
|
3829
|
+
},
|
|
3830
|
+
{
|
|
3831
|
+
capabilities: {},
|
|
3832
|
+
},
|
|
3833
|
+
);
|
|
3834
|
+
|
|
3835
|
+
const client2 = new Client(
|
|
3836
|
+
{
|
|
3837
|
+
name: "Client 2",
|
|
3838
|
+
version: "1.0.0",
|
|
3839
|
+
},
|
|
3840
|
+
{
|
|
3841
|
+
capabilities: {},
|
|
3842
|
+
},
|
|
3843
|
+
);
|
|
3844
|
+
|
|
3845
|
+
const transport1 = new StreamableHTTPClientTransport(
|
|
3846
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
3847
|
+
);
|
|
3848
|
+
|
|
3849
|
+
const transport2 = new StreamableHTTPClientTransport(
|
|
3850
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
3851
|
+
);
|
|
3852
|
+
|
|
3853
|
+
await client1.connect(transport1);
|
|
3854
|
+
await client2.connect(transport2);
|
|
3855
|
+
|
|
3856
|
+
// Both clients should work independently
|
|
3857
|
+
const result1 = await client1.callTool({
|
|
3858
|
+
arguments: {},
|
|
3859
|
+
name: "whoami",
|
|
3860
|
+
});
|
|
3861
|
+
|
|
3862
|
+
const result2 = await client2.callTool({
|
|
3863
|
+
arguments: {},
|
|
3864
|
+
name: "whoami",
|
|
3865
|
+
});
|
|
3866
|
+
|
|
3867
|
+
// Each request should have been authenticated
|
|
3868
|
+
expect((result1.content as unknown[])[0]).toHaveProperty("text");
|
|
3869
|
+
expect((result2.content as unknown[])[0]).toHaveProperty("text");
|
|
3870
|
+
|
|
3871
|
+
// Server should not track sessions in stateless mode
|
|
3872
|
+
expect(server.sessions.length).toBe(0);
|
|
3873
|
+
|
|
3874
|
+
await client1.close();
|
|
3875
|
+
await client2.close();
|
|
3876
|
+
} finally {
|
|
3877
|
+
await server.stop();
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
|
|
3296
3881
|
test("host configuration works with 0.0.0.0", async () => {
|
|
3297
3882
|
const port = await getRandomPort();
|
|
3298
3883
|
|
package/src/FastMCP.ts
CHANGED
|
@@ -1987,48 +1987,30 @@ export class FastMCP<
|
|
|
1987
1987
|
|
|
1988
1988
|
// Try to match against resource templates
|
|
1989
1989
|
for (const template of this.#resourcesTemplates) {
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
const templateParts = template.uriTemplate.split("/");
|
|
1996
|
-
const uriParts = uri.split("/");
|
|
1997
|
-
|
|
1998
|
-
for (let i = 0; i < templateParts.length; i++) {
|
|
1999
|
-
const templatePart = templateParts[i];
|
|
2000
|
-
|
|
2001
|
-
if (templatePart?.startsWith("{") && templatePart.endsWith("}")) {
|
|
2002
|
-
const paramName = templatePart.slice(1, -1);
|
|
2003
|
-
const paramValue = uriParts[i];
|
|
2004
|
-
|
|
2005
|
-
if (paramValue) {
|
|
2006
|
-
params[paramName] = paramValue;
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
const result = await template.load(
|
|
2012
|
-
params as ResourceTemplateArgumentsToObject<
|
|
2013
|
-
typeof template.arguments
|
|
2014
|
-
>,
|
|
2015
|
-
);
|
|
1990
|
+
const parsedTemplate = parseURITemplate(template.uriTemplate);
|
|
1991
|
+
const params = parsedTemplate.fromUri(uri);
|
|
1992
|
+
if (!params) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
2016
1995
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
};
|
|
1996
|
+
const result = await template.load(
|
|
1997
|
+
params as ResourceTemplateArgumentsToObject<typeof template.arguments>,
|
|
1998
|
+
);
|
|
2021
1999
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2000
|
+
const resourceData: ResourceContent["resource"] = {
|
|
2001
|
+
mimeType: template.mimeType,
|
|
2002
|
+
uri,
|
|
2003
|
+
};
|
|
2025
2004
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2005
|
+
if ("text" in result) {
|
|
2006
|
+
resourceData.text = result.text;
|
|
2007
|
+
}
|
|
2029
2008
|
|
|
2030
|
-
|
|
2009
|
+
if ("blob" in result) {
|
|
2010
|
+
resourceData.blob = result.blob;
|
|
2031
2011
|
}
|
|
2012
|
+
|
|
2013
|
+
return resourceData; // The resource we're looking for
|
|
2032
2014
|
}
|
|
2033
2015
|
|
|
2034
2016
|
throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
|
|
@@ -2127,11 +2109,18 @@ export class FastMCP<
|
|
|
2127
2109
|
);
|
|
2128
2110
|
|
|
2129
2111
|
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2112
|
+
authenticate: this.#authenticate,
|
|
2130
2113
|
createServer: async (request) => {
|
|
2131
2114
|
let auth: T | undefined;
|
|
2132
2115
|
|
|
2133
2116
|
if (this.#authenticate) {
|
|
2134
2117
|
auth = await this.#authenticate(request);
|
|
2118
|
+
|
|
2119
|
+
// In stateless mode, authentication is REQUIRED
|
|
2120
|
+
// mcp-proxy will catch this error and return 401
|
|
2121
|
+
if (auth === undefined || auth === null) {
|
|
2122
|
+
throw new Error("Authentication required");
|
|
2123
|
+
}
|
|
2135
2124
|
}
|
|
2136
2125
|
|
|
2137
2126
|
// In stateless mode, create a new session for each request
|
|
@@ -2161,6 +2150,7 @@ export class FastMCP<
|
|
|
2161
2150
|
} else {
|
|
2162
2151
|
// Regular mode with session management
|
|
2163
2152
|
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2153
|
+
authenticate: this.#authenticate,
|
|
2164
2154
|
createServer: async (request) => {
|
|
2165
2155
|
let auth: T | undefined;
|
|
2166
2156
|
|
|
@@ -2201,6 +2191,7 @@ export class FastMCP<
|
|
|
2201
2191
|
);
|
|
2202
2192
|
},
|
|
2203
2193
|
port: httpConfig.port,
|
|
2194
|
+
stateless: httpConfig.stateless,
|
|
2204
2195
|
streamEndpoint: httpConfig.endpoint,
|
|
2205
2196
|
});
|
|
2206
2197
|
|