fastmcp 3.20.1 → 3.21.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.
@@ -1,50 +0,0 @@
1
- name: Release
2
- on:
3
- push:
4
- branches:
5
- - main
6
- jobs:
7
- test:
8
- environment: release
9
- name: Test
10
- strategy:
11
- fail-fast: true
12
- matrix:
13
- node:
14
- - 22
15
- runs-on: ubuntu-latest
16
- permissions:
17
- contents: write
18
- id-token: write
19
- steps:
20
- - name: setup repository
21
- uses: actions/checkout@v4
22
- with:
23
- fetch-depth: 0
24
- - uses: pnpm/action-setup@v4
25
- with:
26
- version: 9
27
- - name: setup node.js
28
- uses: actions/setup-node@v4
29
- with:
30
- cache: "pnpm"
31
- node-version: ${{ matrix.node }}
32
- - name: Setup NodeJS ${{ matrix.node }}
33
- uses: actions/setup-node@v4
34
- with:
35
- node-version: ${{ matrix.node }}
36
- cache: "pnpm"
37
- cache-dependency-path: "**/pnpm-lock.yaml"
38
- - name: Install dependencies
39
- run: pnpm install
40
- - name: Run lint
41
- run: pnpm lint
42
- - name: Run tests
43
- run: pnpm test
44
- - name: Build
45
- run: pnpm build
46
- - name: Release
47
- run: pnpm semantic-release
48
- env:
49
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.prettierignore DELETED
@@ -1 +0,0 @@
1
- pnpm-lock.yaml
package/.roo/mcp.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "context7": {
4
- "command": "npx",
5
- "args": ["-y", "@upstash/context7-mcp"],
6
- "env": {
7
- "DEFAULT_MINIMUM_TOKENS": ""
8
- }
9
- }
10
- }
11
- }
package/eslint.config.ts DELETED
@@ -1,14 +0,0 @@
1
- import eslint from "@eslint/js";
2
- import eslintConfigPrettier from "eslint-config-prettier/flat";
3
- import perfectionist from "eslint-plugin-perfectionist";
4
- import tseslint from "typescript-eslint";
5
-
6
- export default tseslint.config(
7
- eslint.configs.recommended,
8
- tseslint.configs.recommended,
9
- perfectionist.configs["recommended-alphabetical"],
10
- eslintConfigPrettier,
11
- {
12
- ignores: ["**/*.js", "dist/**"],
13
- },
14
- );
package/jsr.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "exports": "./src/FastMCP.ts",
3
- "include": ["src/FastMCP.ts", "src/bin/fastmcp.ts"],
4
- "license": "MIT",
5
- "name": "@punkpeye/fastmcp",
6
- "version": "3.20.1"
7
- }
@@ -1,225 +0,0 @@
1
- import { getRandomPort } from "get-port-please";
2
- import { describe, expect, it } from "vitest";
3
-
4
- import { FastMCP } from "./FastMCP.js";
5
-
6
- describe("FastMCP OAuth Support", () => {
7
- it("should serve OAuth authorization server metadata", async () => {
8
- const port = await getRandomPort();
9
-
10
- const server = new FastMCP({
11
- name: "Test Server",
12
- oauth: {
13
- authorizationServer: {
14
- authorizationEndpoint: "https://auth.example.com/oauth/authorize",
15
- dpopSigningAlgValuesSupported: ["ES256", "RS256"],
16
- grantTypesSupported: ["authorization_code", "refresh_token"],
17
- issuer: "https://auth.example.com",
18
- jwksUri: "https://auth.example.com/.well-known/jwks.json",
19
- responseTypesSupported: ["code"],
20
- scopesSupported: ["read", "write"],
21
- tokenEndpoint: "https://auth.example.com/oauth/token",
22
- },
23
- enabled: true,
24
- },
25
- version: "1.0.0",
26
- });
27
-
28
- await server.start({
29
- httpStream: { port },
30
- transportType: "httpStream",
31
- });
32
-
33
- try {
34
- // Test the OAuth authorization server endpoint
35
- const response = await fetch(
36
- `http://localhost:${port}/.well-known/oauth-authorization-server`,
37
- );
38
- expect(response.status).toBe(200);
39
- expect(response.headers.get("content-type")).toBe("application/json");
40
-
41
- const metadata = (await response.json()) as Record<string, unknown>;
42
-
43
- // Check that camelCase was converted to snake_case
44
- expect(metadata.issuer).toBe("https://auth.example.com");
45
- expect(metadata.authorization_endpoint).toBe(
46
- "https://auth.example.com/oauth/authorize",
47
- );
48
- expect(metadata.token_endpoint).toBe(
49
- "https://auth.example.com/oauth/token",
50
- );
51
- expect(metadata.response_types_supported).toEqual(["code"]);
52
- expect(metadata.jwks_uri).toBe(
53
- "https://auth.example.com/.well-known/jwks.json",
54
- );
55
- expect(metadata.scopes_supported).toEqual(["read", "write"]);
56
- expect(metadata.grant_types_supported).toEqual([
57
- "authorization_code",
58
- "refresh_token",
59
- ]);
60
- expect(metadata.dpop_signing_alg_values_supported).toEqual([
61
- "ES256",
62
- "RS256",
63
- ]);
64
- } finally {
65
- await server.stop();
66
- }
67
- });
68
-
69
- it("should serve OAuth protected resource metadata", async () => {
70
- const port = await getRandomPort();
71
-
72
- const server = new FastMCP({
73
- name: "Test Server",
74
- oauth: {
75
- enabled: true,
76
- protectedResource: {
77
- authorizationDetailsTypesSupported: ["payment_initiation"],
78
- authorizationServers: ["https://auth.example.com"],
79
- bearerMethodsSupported: ["header"],
80
- dpopBoundAccessTokensRequired: true,
81
- dpopSigningAlgValuesSupported: ["ES256", "RS256"],
82
- jwksUri: "https://test-server.example.com/.well-known/jwks.json",
83
- resource: "mcp://test-server",
84
- resourceDocumentation: "https://docs.example.com/api",
85
- resourceName: "Test API",
86
- resourcePolicyUri: "https://test-server.example.com/policy",
87
- resourceSigningAlgValuesSupported: ["RS256"],
88
- resourceTosUri: "https://test-server.example.com/tos",
89
- scopesSupported: ["read", "write", "admin"],
90
- serviceDocumentation: "https://developer.example.com/api",
91
- tlsClientCertificateBoundAccessTokens: false,
92
- vendorPrefix_complexObject: {
93
- nestedArray: [1, 2, 3],
94
- nestedProperty: "nested value",
95
- },
96
- // Vendor extensions (dynamic properties)
97
- vendorPrefix_customField: "custom value",
98
- x_api_version: "2.0",
99
- },
100
- },
101
- version: "1.0.0",
102
- });
103
-
104
- await server.start({
105
- httpStream: { port },
106
- transportType: "httpStream",
107
- });
108
-
109
- try {
110
- const response = await fetch(
111
- `http://localhost:${port}/.well-known/oauth-protected-resource`,
112
- );
113
- expect(response.status).toBe(200);
114
- expect(response.headers.get("content-type")).toBe("application/json");
115
-
116
- const metadata = (await response.json()) as Record<string, unknown>;
117
-
118
- // Check that camelCase was converted to snake_case
119
- expect(metadata.resource).toBe("mcp://test-server");
120
- expect(metadata.authorization_servers).toEqual([
121
- "https://auth.example.com",
122
- ]);
123
- expect(metadata.jwks_uri).toBe(
124
- "https://test-server.example.com/.well-known/jwks.json",
125
- );
126
- expect(metadata.bearer_methods_supported).toEqual(["header"]);
127
- expect(metadata.resource_documentation).toBe(
128
- "https://docs.example.com/api",
129
- );
130
-
131
- // New fields added for RFC 9728 compliance
132
- expect(metadata.authorization_details_types_supported).toEqual([
133
- "payment_initiation",
134
- ]);
135
- expect(metadata.dpop_bound_access_tokens_required).toBe(true);
136
- expect(metadata.dpop_signing_alg_values_supported).toEqual([
137
- "ES256",
138
- "RS256",
139
- ]);
140
- expect(metadata.resource_name).toBe("Test API");
141
- expect(metadata.resource_policy_uri).toBe(
142
- "https://test-server.example.com/policy",
143
- );
144
- expect(metadata.resource_signing_alg_values_supported).toEqual(["RS256"]);
145
- expect(metadata.resource_tos_uri).toBe(
146
- "https://test-server.example.com/tos",
147
- );
148
- expect(metadata.scopes_supported).toEqual(["read", "write", "admin"]);
149
- expect(metadata.service_documentation).toBe(
150
- "https://developer.example.com/api",
151
- );
152
- expect(metadata.tls_client_certificate_bound_access_tokens).toBe(false);
153
-
154
- // Vendor extensions (dynamic properties)
155
- expect(metadata.vendor_prefix_custom_field).toBe("custom value");
156
- expect(metadata.vendor_prefix_complex_object).toEqual({
157
- nestedArray: [1, 2, 3],
158
- nestedProperty: "nested value",
159
- });
160
- expect(metadata.x_api_version).toBe("2.0");
161
- } finally {
162
- await server.stop();
163
- }
164
- });
165
-
166
- it("should return 404 for OAuth endpoints when disabled", async () => {
167
- const port = await getRandomPort();
168
-
169
- const server = new FastMCP({
170
- name: "Test Server",
171
- oauth: {
172
- enabled: false,
173
- },
174
- version: "1.0.0",
175
- });
176
-
177
- await server.start({
178
- httpStream: { port },
179
- transportType: "httpStream",
180
- });
181
-
182
- try {
183
- const authServerResponse = await fetch(
184
- `http://localhost:${port}/.well-known/oauth-authorization-server`,
185
- );
186
- expect(authServerResponse.status).toBe(404);
187
-
188
- const protectedResourceResponse = await fetch(
189
- `http://localhost:${port}/.well-known/oauth-protected-resource`,
190
- );
191
- expect(protectedResourceResponse.status).toBe(404);
192
- } finally {
193
- await server.stop();
194
- }
195
- });
196
-
197
- it("should return 404 for OAuth endpoints when not configured", async () => {
198
- const port = await getRandomPort();
199
-
200
- const server = new FastMCP({
201
- name: "Test Server",
202
- version: "1.0.0",
203
- // No oauth configuration
204
- });
205
-
206
- await server.start({
207
- httpStream: { port },
208
- transportType: "httpStream",
209
- });
210
-
211
- try {
212
- const authServerResponse = await fetch(
213
- `http://localhost:${port}/.well-known/oauth-authorization-server`,
214
- );
215
- expect(authServerResponse.status).toBe(404);
216
-
217
- const protectedResourceResponse = await fetch(
218
- `http://localhost:${port}/.well-known/oauth-protected-resource`,
219
- );
220
- expect(protectedResourceResponse.status).toBe(404);
221
- } finally {
222
- await server.stop();
223
- }
224
- });
225
- });
@@ -1,136 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { z } from "zod";
3
-
4
- import { FastMCP } from "./FastMCP.js";
5
-
6
- interface TestAuth {
7
- [key: string]: unknown; // Required for FastMCPSessionAuth compatibility
8
- role: "admin" | "user";
9
- userId: string;
10
- }
11
-
12
- describe("FastMCP Session Context", () => {
13
- describe("stdio transport", () => {
14
- it("should pass session context to tool execution when authenticate is provided", async () => {
15
- const mockAuth: TestAuth = { role: "admin", userId: "test-user" };
16
- const server = new FastMCP<TestAuth>({
17
- authenticate: async (request) => {
18
- if (!request) return mockAuth;
19
-
20
- throw new Error("Unexpected request in test");
21
- },
22
- name: "test-server",
23
- version: "1.0.0",
24
- });
25
-
26
- server.addTool({
27
- description: "Test tool to verify session context",
28
- execute: async (_args, context) => {
29
- return `Session received: ${context.session ? "yes" : "no"}`;
30
- },
31
- name: "test-session-context",
32
- parameters: z.object({
33
- message: z.string(),
34
- }),
35
- });
36
-
37
- await server.start({ transportType: "stdio" });
38
-
39
- expect(server).toBeDefined();
40
- });
41
-
42
- it("should handle authentication errors gracefully in stdio transport", async () => {
43
- const mockLogger = {
44
- debug: vi.fn(),
45
- error: vi.fn(),
46
- info: vi.fn(),
47
- log: vi.fn(),
48
- warn: vi.fn(),
49
- };
50
- const server = new FastMCP<TestAuth>({
51
- authenticate: async () => {
52
- throw new Error("Auth failed");
53
- },
54
- logger: mockLogger,
55
- name: "test-server",
56
- version: "1.0.0",
57
- });
58
-
59
- server.addTool({
60
- description: "Test tool",
61
- execute: async (_args, context) => {
62
- return `Session: ${context.session ? "present" : "undefined"}`;
63
- },
64
- name: "test-tool",
65
- });
66
-
67
- await server.start({ transportType: "stdio" });
68
-
69
- expect(mockLogger.error).toHaveBeenCalledWith(
70
- "[FastMCP error] Authentication failed for stdio transport:",
71
- "Auth failed",
72
- );
73
- });
74
-
75
- it("should work without authenticate function", async () => {
76
- const server = new FastMCP({
77
- name: "test-server",
78
- version: "1.0.0",
79
- });
80
-
81
- server.addTool({
82
- description: "Test tool without auth",
83
- execute: async (_args, context) => {
84
- return `Session: ${context.session ? "present" : "undefined"}`;
85
- },
86
- name: "test-tool",
87
- });
88
-
89
- await server.start({ transportType: "stdio" });
90
-
91
- expect(server).toBeDefined();
92
- });
93
- });
94
-
95
- describe("environment variable based authentication", () => {
96
- it("should support reading from environment variables in stdio mode", async () => {
97
- const originalEnv = process.env.TEST_USER_ID;
98
-
99
- process.env.TEST_USER_ID = "env-user-123";
100
-
101
- try {
102
- const server = new FastMCP<TestAuth>({
103
- authenticate: async (request) => {
104
- if (!request) {
105
- return {
106
- role: "user" as const,
107
- userId: process.env.TEST_USER_ID || "default-user",
108
- };
109
- }
110
- throw new Error("HTTP not supported in this test");
111
- },
112
- name: "test-server",
113
- version: "1.0.0",
114
- });
115
-
116
- server.addTool({
117
- description: "Tool using env-based auth",
118
- execute: async (_args, context) => {
119
- return `Environment user: ${context.session?.userId}`;
120
- },
121
- name: "env-test-tool",
122
- });
123
-
124
- await server.start({ transportType: "stdio" });
125
-
126
- expect(server).toBeDefined();
127
- } finally {
128
- if (originalEnv !== undefined) {
129
- process.env.TEST_USER_ID = originalEnv;
130
- } else {
131
- delete process.env.TEST_USER_ID;
132
- }
133
- }
134
- });
135
- });
136
- });