fastmcp 3.19.2 → 3.20.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/.roo/mcp.json +11 -0
- package/README.md +83 -0
- package/dist/FastMCP.d.ts +16 -1
- package/dist/FastMCP.js +33 -5
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/FastMCP.session-id.test.ts +359 -0
- package/src/FastMCP.test.ts +449 -1
- package/src/FastMCP.ts +73 -5
- package/src/examples/session-id-counter.ts +230 -0
package/src/FastMCP.test.ts
CHANGED
|
@@ -2416,7 +2416,9 @@ test("provides auth to tools", async () => {
|
|
|
2416
2416
|
warn: expect.any(Function),
|
|
2417
2417
|
},
|
|
2418
2418
|
reportProgress: expect.any(Function),
|
|
2419
|
+
requestId: undefined,
|
|
2419
2420
|
session: { id: 1 },
|
|
2421
|
+
sessionId: expect.any(String),
|
|
2420
2422
|
streamContent: expect.any(Function),
|
|
2421
2423
|
},
|
|
2422
2424
|
);
|
|
@@ -3783,7 +3785,8 @@ test("stateless mode handles authentication function throwing errors", async ()
|
|
|
3783
3785
|
expect(response.status).toBe(401);
|
|
3784
3786
|
|
|
3785
3787
|
const body = (await response.json()) as { error?: { message?: string } };
|
|
3786
|
-
|
|
3788
|
+
// The actual error message should be passed through
|
|
3789
|
+
expect(body.error?.message).toContain("JWT validation service is down");
|
|
3787
3790
|
} finally {
|
|
3788
3791
|
await server.stop();
|
|
3789
3792
|
}
|
|
@@ -3878,6 +3881,451 @@ test("stateless mode handles concurrent requests with authentication", async ()
|
|
|
3878
3881
|
}
|
|
3879
3882
|
});
|
|
3880
3883
|
|
|
3884
|
+
// Tests for GitHub Issue: FastMCP authentication fix
|
|
3885
|
+
// Testing the fix for session creation despite authentication failure
|
|
3886
|
+
|
|
3887
|
+
test("authentication failure handling: should throw error when auth.authenticated is false", async () => {
|
|
3888
|
+
const port = await getRandomPort();
|
|
3889
|
+
|
|
3890
|
+
const server = new FastMCP<{ authenticated: boolean; error?: string }>({
|
|
3891
|
+
authenticate: async () => {
|
|
3892
|
+
// Simulate authentication failure with { authenticated: false }
|
|
3893
|
+
return { authenticated: false, error: "Invalid JWT token" };
|
|
3894
|
+
},
|
|
3895
|
+
name: "Test server",
|
|
3896
|
+
version: "1.0.0",
|
|
3897
|
+
});
|
|
3898
|
+
|
|
3899
|
+
server.addTool({
|
|
3900
|
+
description: "Test tool",
|
|
3901
|
+
execute: async () => {
|
|
3902
|
+
return "pong";
|
|
3903
|
+
},
|
|
3904
|
+
name: "ping",
|
|
3905
|
+
parameters: z.object({}),
|
|
3906
|
+
});
|
|
3907
|
+
|
|
3908
|
+
await server.start({
|
|
3909
|
+
httpStream: {
|
|
3910
|
+
port,
|
|
3911
|
+
stateless: true,
|
|
3912
|
+
},
|
|
3913
|
+
transportType: "httpStream",
|
|
3914
|
+
});
|
|
3915
|
+
|
|
3916
|
+
try {
|
|
3917
|
+
// Send a raw HTTP request that should be rejected
|
|
3918
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
3919
|
+
body: JSON.stringify({
|
|
3920
|
+
id: 1,
|
|
3921
|
+
jsonrpc: "2.0",
|
|
3922
|
+
method: "initialize",
|
|
3923
|
+
params: {
|
|
3924
|
+
capabilities: {},
|
|
3925
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
3926
|
+
protocolVersion: "2024-11-05",
|
|
3927
|
+
},
|
|
3928
|
+
}),
|
|
3929
|
+
headers: {
|
|
3930
|
+
Accept: "application/json, text/event-stream",
|
|
3931
|
+
Authorization: "Bearer invalid.jwt.token",
|
|
3932
|
+
"Content-Type": "application/json",
|
|
3933
|
+
},
|
|
3934
|
+
method: "POST",
|
|
3935
|
+
});
|
|
3936
|
+
|
|
3937
|
+
// Should return 401 Unauthorized (handled by mcp-proxy)
|
|
3938
|
+
expect(response.status).toBe(401);
|
|
3939
|
+
|
|
3940
|
+
const body = (await response.json()) as {
|
|
3941
|
+
error?: { code?: number; message?: string };
|
|
3942
|
+
};
|
|
3943
|
+
expect(body.error?.message).toContain("Invalid JWT token");
|
|
3944
|
+
} finally {
|
|
3945
|
+
await server.stop();
|
|
3946
|
+
}
|
|
3947
|
+
});
|
|
3948
|
+
|
|
3949
|
+
test("authentication failure handling: should create session when auth.authenticated is true", async () => {
|
|
3950
|
+
const port = await getRandomPort();
|
|
3951
|
+
|
|
3952
|
+
const server = new FastMCP<{
|
|
3953
|
+
authenticated: boolean;
|
|
3954
|
+
session?: { userId: string };
|
|
3955
|
+
}>({
|
|
3956
|
+
authenticate: async () => {
|
|
3957
|
+
// Simulate successful authentication
|
|
3958
|
+
return { authenticated: true, session: { userId: "123" } };
|
|
3959
|
+
},
|
|
3960
|
+
name: "Test server",
|
|
3961
|
+
version: "1.0.0",
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
server.addTool({
|
|
3965
|
+
description: "Test tool",
|
|
3966
|
+
execute: async (_args, context) => {
|
|
3967
|
+
return `User: ${context.session?.session?.userId}`;
|
|
3968
|
+
},
|
|
3969
|
+
name: "whoami",
|
|
3970
|
+
parameters: z.object({}),
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
await server.start({
|
|
3974
|
+
httpStream: {
|
|
3975
|
+
port,
|
|
3976
|
+
stateless: true,
|
|
3977
|
+
},
|
|
3978
|
+
transportType: "httpStream",
|
|
3979
|
+
});
|
|
3980
|
+
|
|
3981
|
+
try {
|
|
3982
|
+
const client = new Client(
|
|
3983
|
+
{
|
|
3984
|
+
name: "Test client",
|
|
3985
|
+
version: "1.0.0",
|
|
3986
|
+
},
|
|
3987
|
+
{
|
|
3988
|
+
capabilities: {},
|
|
3989
|
+
},
|
|
3990
|
+
);
|
|
3991
|
+
|
|
3992
|
+
const transport = new StreamableHTTPClientTransport(
|
|
3993
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
3994
|
+
);
|
|
3995
|
+
|
|
3996
|
+
await client.connect(transport);
|
|
3997
|
+
|
|
3998
|
+
const result = await client.callTool({
|
|
3999
|
+
arguments: {},
|
|
4000
|
+
name: "whoami",
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
expect(result.content).toEqual([
|
|
4004
|
+
{
|
|
4005
|
+
text: "User: 123",
|
|
4006
|
+
type: "text",
|
|
4007
|
+
},
|
|
4008
|
+
]);
|
|
4009
|
+
|
|
4010
|
+
await client.close();
|
|
4011
|
+
} finally {
|
|
4012
|
+
await server.stop();
|
|
4013
|
+
}
|
|
4014
|
+
});
|
|
4015
|
+
|
|
4016
|
+
test("authentication failure handling: should create session when auth is null/undefined (anonymous)", async () => {
|
|
4017
|
+
const port = await getRandomPort();
|
|
4018
|
+
|
|
4019
|
+
const server = new FastMCP({
|
|
4020
|
+
// No authenticate function - anonymous access
|
|
4021
|
+
name: "Test server",
|
|
4022
|
+
version: "1.0.0",
|
|
4023
|
+
});
|
|
4024
|
+
|
|
4025
|
+
server.addTool({
|
|
4026
|
+
description: "Test tool",
|
|
4027
|
+
execute: async (_args, context) => {
|
|
4028
|
+
return `Anonymous: ${context.session === undefined}`;
|
|
4029
|
+
},
|
|
4030
|
+
name: "ping",
|
|
4031
|
+
parameters: z.object({}),
|
|
4032
|
+
});
|
|
4033
|
+
|
|
4034
|
+
await server.start({
|
|
4035
|
+
httpStream: {
|
|
4036
|
+
port,
|
|
4037
|
+
stateless: true,
|
|
4038
|
+
},
|
|
4039
|
+
transportType: "httpStream",
|
|
4040
|
+
});
|
|
4041
|
+
|
|
4042
|
+
try {
|
|
4043
|
+
const client = new Client(
|
|
4044
|
+
{
|
|
4045
|
+
name: "Test client",
|
|
4046
|
+
version: "1.0.0",
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
capabilities: {},
|
|
4050
|
+
},
|
|
4051
|
+
);
|
|
4052
|
+
|
|
4053
|
+
const transport = new StreamableHTTPClientTransport(
|
|
4054
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
4055
|
+
);
|
|
4056
|
+
|
|
4057
|
+
await client.connect(transport);
|
|
4058
|
+
|
|
4059
|
+
const result = await client.callTool({
|
|
4060
|
+
arguments: {},
|
|
4061
|
+
name: "ping",
|
|
4062
|
+
});
|
|
4063
|
+
|
|
4064
|
+
expect(result.content).toEqual([
|
|
4065
|
+
{
|
|
4066
|
+
text: "Anonymous: true",
|
|
4067
|
+
type: "text",
|
|
4068
|
+
},
|
|
4069
|
+
]);
|
|
4070
|
+
|
|
4071
|
+
await client.close();
|
|
4072
|
+
} finally {
|
|
4073
|
+
await server.stop();
|
|
4074
|
+
}
|
|
4075
|
+
});
|
|
4076
|
+
|
|
4077
|
+
test("authentication failure handling: should use default error message when auth.error is not provided", async () => {
|
|
4078
|
+
const port = await getRandomPort();
|
|
4079
|
+
|
|
4080
|
+
const server = new FastMCP<{ authenticated: boolean }>({
|
|
4081
|
+
authenticate: async () => {
|
|
4082
|
+
// Return authenticated: false without custom error message
|
|
4083
|
+
return { authenticated: false };
|
|
4084
|
+
},
|
|
4085
|
+
name: "Test server",
|
|
4086
|
+
version: "1.0.0",
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
server.addTool({
|
|
4090
|
+
description: "Test tool",
|
|
4091
|
+
execute: async () => {
|
|
4092
|
+
return "pong";
|
|
4093
|
+
},
|
|
4094
|
+
name: "ping",
|
|
4095
|
+
parameters: z.object({}),
|
|
4096
|
+
});
|
|
4097
|
+
|
|
4098
|
+
await server.start({
|
|
4099
|
+
httpStream: {
|
|
4100
|
+
port,
|
|
4101
|
+
stateless: true,
|
|
4102
|
+
},
|
|
4103
|
+
transportType: "httpStream",
|
|
4104
|
+
});
|
|
4105
|
+
|
|
4106
|
+
try {
|
|
4107
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
4108
|
+
body: JSON.stringify({
|
|
4109
|
+
id: 1,
|
|
4110
|
+
jsonrpc: "2.0",
|
|
4111
|
+
method: "initialize",
|
|
4112
|
+
params: {
|
|
4113
|
+
capabilities: {},
|
|
4114
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
4115
|
+
protocolVersion: "2024-11-05",
|
|
4116
|
+
},
|
|
4117
|
+
}),
|
|
4118
|
+
headers: {
|
|
4119
|
+
Accept: "application/json, text/event-stream",
|
|
4120
|
+
"Content-Type": "application/json",
|
|
4121
|
+
},
|
|
4122
|
+
method: "POST",
|
|
4123
|
+
});
|
|
4124
|
+
|
|
4125
|
+
expect(response.status).toBe(401);
|
|
4126
|
+
|
|
4127
|
+
const body = (await response.json()) as {
|
|
4128
|
+
error?: { message?: string };
|
|
4129
|
+
};
|
|
4130
|
+
expect(body.error?.message).toContain("Authentication failed");
|
|
4131
|
+
} finally {
|
|
4132
|
+
await server.stop();
|
|
4133
|
+
}
|
|
4134
|
+
});
|
|
4135
|
+
|
|
4136
|
+
test("authentication failure handling: should preserve existing behavior for truthy auth results", async () => {
|
|
4137
|
+
const port = await getRandomPort();
|
|
4138
|
+
|
|
4139
|
+
const server = new FastMCP<{ role: string; userId: string }>({
|
|
4140
|
+
authenticate: async () => {
|
|
4141
|
+
// Return a truthy object without 'authenticated' field (legacy pattern)
|
|
4142
|
+
return { role: "admin", userId: "456" };
|
|
4143
|
+
},
|
|
4144
|
+
name: "Test server",
|
|
4145
|
+
version: "1.0.0",
|
|
4146
|
+
});
|
|
4147
|
+
|
|
4148
|
+
server.addTool({
|
|
4149
|
+
description: "Test tool",
|
|
4150
|
+
execute: async (_args, context) => {
|
|
4151
|
+
return `User: ${context.session?.userId}, Role: ${context.session?.role}`;
|
|
4152
|
+
},
|
|
4153
|
+
name: "whoami",
|
|
4154
|
+
parameters: z.object({}),
|
|
4155
|
+
});
|
|
4156
|
+
|
|
4157
|
+
await server.start({
|
|
4158
|
+
httpStream: {
|
|
4159
|
+
port,
|
|
4160
|
+
stateless: true,
|
|
4161
|
+
},
|
|
4162
|
+
transportType: "httpStream",
|
|
4163
|
+
});
|
|
4164
|
+
|
|
4165
|
+
try {
|
|
4166
|
+
const client = new Client(
|
|
4167
|
+
{
|
|
4168
|
+
name: "Test client",
|
|
4169
|
+
version: "1.0.0",
|
|
4170
|
+
},
|
|
4171
|
+
{
|
|
4172
|
+
capabilities: {},
|
|
4173
|
+
},
|
|
4174
|
+
);
|
|
4175
|
+
|
|
4176
|
+
const transport = new StreamableHTTPClientTransport(
|
|
4177
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
4178
|
+
);
|
|
4179
|
+
|
|
4180
|
+
await client.connect(transport);
|
|
4181
|
+
|
|
4182
|
+
const result = await client.callTool({
|
|
4183
|
+
arguments: {},
|
|
4184
|
+
name: "whoami",
|
|
4185
|
+
});
|
|
4186
|
+
|
|
4187
|
+
expect(result.content).toEqual([
|
|
4188
|
+
{
|
|
4189
|
+
text: "User: 456, Role: admin",
|
|
4190
|
+
type: "text",
|
|
4191
|
+
},
|
|
4192
|
+
]);
|
|
4193
|
+
|
|
4194
|
+
await client.close();
|
|
4195
|
+
} finally {
|
|
4196
|
+
await server.stop();
|
|
4197
|
+
}
|
|
4198
|
+
});
|
|
4199
|
+
|
|
4200
|
+
test("authentication failure handling: should handle authentication with custom error messages", async () => {
|
|
4201
|
+
const port = await getRandomPort();
|
|
4202
|
+
const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z";
|
|
4203
|
+
|
|
4204
|
+
const server = new FastMCP<{ authenticated: boolean; error?: string }>({
|
|
4205
|
+
authenticate: async () => {
|
|
4206
|
+
return { authenticated: false, error: CUSTOM_ERROR_MSG };
|
|
4207
|
+
},
|
|
4208
|
+
name: "Test server",
|
|
4209
|
+
version: "1.0.0",
|
|
4210
|
+
});
|
|
4211
|
+
|
|
4212
|
+
server.addTool({
|
|
4213
|
+
description: "Test tool",
|
|
4214
|
+
execute: async () => {
|
|
4215
|
+
return "pong";
|
|
4216
|
+
},
|
|
4217
|
+
name: "ping",
|
|
4218
|
+
parameters: z.object({}),
|
|
4219
|
+
});
|
|
4220
|
+
|
|
4221
|
+
await server.start({
|
|
4222
|
+
httpStream: {
|
|
4223
|
+
port,
|
|
4224
|
+
stateless: true,
|
|
4225
|
+
},
|
|
4226
|
+
transportType: "httpStream",
|
|
4227
|
+
});
|
|
4228
|
+
|
|
4229
|
+
try {
|
|
4230
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
4231
|
+
body: JSON.stringify({
|
|
4232
|
+
id: 1,
|
|
4233
|
+
jsonrpc: "2.0",
|
|
4234
|
+
method: "initialize",
|
|
4235
|
+
params: {
|
|
4236
|
+
capabilities: {},
|
|
4237
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
4238
|
+
protocolVersion: "2024-11-05",
|
|
4239
|
+
},
|
|
4240
|
+
}),
|
|
4241
|
+
headers: {
|
|
4242
|
+
Accept: "application/json, text/event-stream",
|
|
4243
|
+
"Content-Type": "application/json",
|
|
4244
|
+
},
|
|
4245
|
+
method: "POST",
|
|
4246
|
+
});
|
|
4247
|
+
|
|
4248
|
+
expect(response.status).toBe(401);
|
|
4249
|
+
|
|
4250
|
+
const body = (await response.json()) as {
|
|
4251
|
+
error?: { message?: string };
|
|
4252
|
+
};
|
|
4253
|
+
expect(body.error?.message).toBe(CUSTOM_ERROR_MSG);
|
|
4254
|
+
} finally {
|
|
4255
|
+
await server.stop();
|
|
4256
|
+
}
|
|
4257
|
+
});
|
|
4258
|
+
|
|
4259
|
+
test("authentication failure handling: should not create session for authenticated=false even with session data", async () => {
|
|
4260
|
+
const port = await getRandomPort();
|
|
4261
|
+
|
|
4262
|
+
const server = new FastMCP<{
|
|
4263
|
+
authenticated: boolean;
|
|
4264
|
+
error?: string;
|
|
4265
|
+
session?: { userId: string };
|
|
4266
|
+
}>({
|
|
4267
|
+
authenticate: async () => {
|
|
4268
|
+
// Even if session data is present, authenticated: false should reject
|
|
4269
|
+
return {
|
|
4270
|
+
authenticated: false,
|
|
4271
|
+
error: "Insufficient permissions",
|
|
4272
|
+
session: { userId: "hacker" },
|
|
4273
|
+
};
|
|
4274
|
+
},
|
|
4275
|
+
name: "Test server",
|
|
4276
|
+
version: "1.0.0",
|
|
4277
|
+
});
|
|
4278
|
+
|
|
4279
|
+
server.addTool({
|
|
4280
|
+
description: "Test tool",
|
|
4281
|
+
execute: async () => {
|
|
4282
|
+
return "pong";
|
|
4283
|
+
},
|
|
4284
|
+
name: "ping",
|
|
4285
|
+
parameters: z.object({}),
|
|
4286
|
+
});
|
|
4287
|
+
|
|
4288
|
+
await server.start({
|
|
4289
|
+
httpStream: {
|
|
4290
|
+
port,
|
|
4291
|
+
stateless: true,
|
|
4292
|
+
},
|
|
4293
|
+
transportType: "httpStream",
|
|
4294
|
+
});
|
|
4295
|
+
|
|
4296
|
+
try {
|
|
4297
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
4298
|
+
body: JSON.stringify({
|
|
4299
|
+
id: 1,
|
|
4300
|
+
jsonrpc: "2.0",
|
|
4301
|
+
method: "initialize",
|
|
4302
|
+
params: {
|
|
4303
|
+
capabilities: {},
|
|
4304
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
4305
|
+
protocolVersion: "2024-11-05",
|
|
4306
|
+
},
|
|
4307
|
+
}),
|
|
4308
|
+
headers: {
|
|
4309
|
+
Accept: "application/json, text/event-stream",
|
|
4310
|
+
"Content-Type": "application/json",
|
|
4311
|
+
},
|
|
4312
|
+
method: "POST",
|
|
4313
|
+
});
|
|
4314
|
+
|
|
4315
|
+
expect(response.status).toBe(401);
|
|
4316
|
+
|
|
4317
|
+
const body = (await response.json()) as {
|
|
4318
|
+
error?: { message?: string };
|
|
4319
|
+
};
|
|
4320
|
+
expect(body.error?.message).toContain("Insufficient permissions");
|
|
4321
|
+
|
|
4322
|
+
// Verify session was never created
|
|
4323
|
+
expect(server.sessions.length).toBe(0);
|
|
4324
|
+
} finally {
|
|
4325
|
+
await server.stop();
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
|
|
3881
4329
|
test("host configuration works with 0.0.0.0", async () => {
|
|
3882
4330
|
const port = await getRandomPort();
|
|
3883
4331
|
|
package/src/FastMCP.ts
CHANGED
|
@@ -210,7 +210,19 @@ type Context<T extends FastMCPSessionAuth> = {
|
|
|
210
210
|
warn: (message: string, data?: SerializableValue) => void;
|
|
211
211
|
};
|
|
212
212
|
reportProgress: (progress: Progress) => Promise<void>;
|
|
213
|
+
/**
|
|
214
|
+
* Request ID from the current MCP request.
|
|
215
|
+
* Available for all transports when the client provides it.
|
|
216
|
+
*/
|
|
217
|
+
requestId?: string;
|
|
213
218
|
session: T | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* Session ID from the Mcp-Session-Id header.
|
|
221
|
+
* Only available for HTTP-based transports (SSE, HTTP Stream).
|
|
222
|
+
* Can be used to track per-session state, implement session-specific
|
|
223
|
+
* counters, or maintain user-specific data across multiple requests.
|
|
224
|
+
*/
|
|
225
|
+
sessionId?: string;
|
|
214
226
|
streamContent: (content: Content | Content[]) => Promise<void>;
|
|
215
227
|
};
|
|
216
228
|
|
|
@@ -936,6 +948,12 @@ export class FastMCPSession<
|
|
|
936
948
|
public get server(): Server {
|
|
937
949
|
return this.#server;
|
|
938
950
|
}
|
|
951
|
+
public get sessionId(): string | undefined {
|
|
952
|
+
return this.#sessionId;
|
|
953
|
+
}
|
|
954
|
+
public set sessionId(value: string | undefined) {
|
|
955
|
+
this.#sessionId = value;
|
|
956
|
+
}
|
|
939
957
|
#auth: T | undefined;
|
|
940
958
|
#capabilities: ServerCapabilities = {};
|
|
941
959
|
#clientCapabilities?: ClientCapabilities;
|
|
@@ -959,6 +977,12 @@ export class FastMCPSession<
|
|
|
959
977
|
|
|
960
978
|
#server: Server;
|
|
961
979
|
|
|
980
|
+
/**
|
|
981
|
+
* Session ID from the Mcp-Session-Id header (HTTP transports only).
|
|
982
|
+
* Used to track per-session state across multiple requests.
|
|
983
|
+
*/
|
|
984
|
+
#sessionId?: string;
|
|
985
|
+
|
|
962
986
|
#utils?: ServerOptions<T>["utils"];
|
|
963
987
|
|
|
964
988
|
constructor({
|
|
@@ -971,6 +995,7 @@ export class FastMCPSession<
|
|
|
971
995
|
resources,
|
|
972
996
|
resourcesTemplates,
|
|
973
997
|
roots,
|
|
998
|
+
sessionId,
|
|
974
999
|
tools,
|
|
975
1000
|
transportType,
|
|
976
1001
|
utils,
|
|
@@ -985,6 +1010,7 @@ export class FastMCPSession<
|
|
|
985
1010
|
resources: Resource<T>[];
|
|
986
1011
|
resourcesTemplates: InputResourceTemplate<T>[];
|
|
987
1012
|
roots?: ServerOptions<T>["roots"];
|
|
1013
|
+
sessionId?: string;
|
|
988
1014
|
tools: Tool<T>[];
|
|
989
1015
|
transportType?: "httpStream" | "stdio";
|
|
990
1016
|
utils?: ServerOptions<T>["utils"];
|
|
@@ -996,6 +1022,7 @@ export class FastMCPSession<
|
|
|
996
1022
|
this.#logger = logger;
|
|
997
1023
|
this.#pingConfig = ping;
|
|
998
1024
|
this.#rootsConfig = roots;
|
|
1025
|
+
this.#sessionId = sessionId;
|
|
999
1026
|
this.#needsEventLoopFlush = transportType === "httpStream";
|
|
1000
1027
|
|
|
1001
1028
|
if (tools.length) {
|
|
@@ -1077,6 +1104,16 @@ export class FastMCPSession<
|
|
|
1077
1104
|
try {
|
|
1078
1105
|
await this.#server.connect(transport);
|
|
1079
1106
|
|
|
1107
|
+
// Extract session ID from transport if available (HTTP transports only)
|
|
1108
|
+
if ("sessionId" in transport) {
|
|
1109
|
+
const transportWithSessionId = transport as {
|
|
1110
|
+
sessionId?: string;
|
|
1111
|
+
} & Transport;
|
|
1112
|
+
if (typeof transportWithSessionId.sessionId === "string") {
|
|
1113
|
+
this.#sessionId = transportWithSessionId.sessionId;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1080
1117
|
let attempt = 0;
|
|
1081
1118
|
const maxAttempts = 10;
|
|
1082
1119
|
const retryDelay = 100;
|
|
@@ -1789,7 +1826,12 @@ export class FastMCPSession<
|
|
|
1789
1826
|
},
|
|
1790
1827
|
log,
|
|
1791
1828
|
reportProgress,
|
|
1829
|
+
requestId:
|
|
1830
|
+
typeof request.params?._meta?.requestId === "string"
|
|
1831
|
+
? request.params._meta.requestId
|
|
1832
|
+
: undefined,
|
|
1792
1833
|
session: this.#auth,
|
|
1834
|
+
sessionId: this.#sessionId,
|
|
1793
1835
|
streamContent,
|
|
1794
1836
|
});
|
|
1795
1837
|
|
|
@@ -2110,7 +2152,7 @@ export class FastMCP<
|
|
|
2110
2152
|
);
|
|
2111
2153
|
|
|
2112
2154
|
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2113
|
-
authenticate: this.#authenticate,
|
|
2155
|
+
...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
|
|
2114
2156
|
createServer: async (request) => {
|
|
2115
2157
|
let auth: T | undefined;
|
|
2116
2158
|
|
|
@@ -2124,9 +2166,14 @@ export class FastMCP<
|
|
|
2124
2166
|
}
|
|
2125
2167
|
}
|
|
2126
2168
|
|
|
2169
|
+
// Extract session ID from headers
|
|
2170
|
+
const sessionId = Array.isArray(request.headers["mcp-session-id"])
|
|
2171
|
+
? request.headers["mcp-session-id"][0]
|
|
2172
|
+
: request.headers["mcp-session-id"];
|
|
2173
|
+
|
|
2127
2174
|
// In stateless mode, create a new session for each request
|
|
2128
2175
|
// without persisting it in the sessions array
|
|
2129
|
-
return this.#createSession(auth);
|
|
2176
|
+
return this.#createSession(auth, sessionId);
|
|
2130
2177
|
},
|
|
2131
2178
|
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2132
2179
|
eventStore: httpConfig.eventStore,
|
|
@@ -2151,7 +2198,7 @@ export class FastMCP<
|
|
|
2151
2198
|
} else {
|
|
2152
2199
|
// Regular mode with session management
|
|
2153
2200
|
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2154
|
-
authenticate: this.#authenticate,
|
|
2201
|
+
...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
|
|
2155
2202
|
createServer: async (request) => {
|
|
2156
2203
|
let auth: T | undefined;
|
|
2157
2204
|
|
|
@@ -2159,7 +2206,12 @@ export class FastMCP<
|
|
|
2159
2206
|
auth = await this.#authenticate(request);
|
|
2160
2207
|
}
|
|
2161
2208
|
|
|
2162
|
-
|
|
2209
|
+
// Extract session ID from headers
|
|
2210
|
+
const sessionId = Array.isArray(request.headers["mcp-session-id"])
|
|
2211
|
+
? request.headers["mcp-session-id"][0]
|
|
2212
|
+
: request.headers["mcp-session-id"];
|
|
2213
|
+
|
|
2214
|
+
return this.#createSession(auth, sessionId);
|
|
2163
2215
|
},
|
|
2164
2216
|
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2165
2217
|
eventStore: httpConfig.eventStore,
|
|
@@ -2218,7 +2270,22 @@ export class FastMCP<
|
|
|
2218
2270
|
* Creates a new FastMCPSession instance with the current configuration.
|
|
2219
2271
|
* Used both for regular sessions and stateless requests.
|
|
2220
2272
|
*/
|
|
2221
|
-
#createSession(auth?: T): FastMCPSession<T> {
|
|
2273
|
+
#createSession(auth?: T, sessionId?: string): FastMCPSession<T> {
|
|
2274
|
+
// Check if authentication failed
|
|
2275
|
+
if (
|
|
2276
|
+
auth &&
|
|
2277
|
+
typeof auth === "object" &&
|
|
2278
|
+
"authenticated" in auth &&
|
|
2279
|
+
!(auth as { authenticated: unknown }).authenticated
|
|
2280
|
+
) {
|
|
2281
|
+
const errorMessage =
|
|
2282
|
+
"error" in auth &&
|
|
2283
|
+
typeof (auth as { error: unknown }).error === "string"
|
|
2284
|
+
? (auth as { error: string }).error
|
|
2285
|
+
: "Authentication failed";
|
|
2286
|
+
throw new Error(errorMessage);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2222
2289
|
const allowedTools = auth
|
|
2223
2290
|
? this.#tools.filter((tool) =>
|
|
2224
2291
|
tool.canAccess ? tool.canAccess(auth) : true,
|
|
@@ -2234,6 +2301,7 @@ export class FastMCP<
|
|
|
2234
2301
|
resources: this.#resources,
|
|
2235
2302
|
resourcesTemplates: this.#resourcesTemplates,
|
|
2236
2303
|
roots: this.#options.roots,
|
|
2304
|
+
sessionId,
|
|
2237
2305
|
tools: allowedTools,
|
|
2238
2306
|
transportType: "httpStream",
|
|
2239
2307
|
utils: this.#options.utils,
|