Merge branch 'main' into claude/issue-2526-20250824-0240

This commit is contained in:
Cliff Hall
2026-02-07 16:57:16 -05:00
committed by GitHub
20 changed files with 3032 additions and 714 deletions

197
LICENSE
View File

@@ -1,6 +1,193 @@
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
No rights beyond those granted by the applicable original license are conveyed for such contributions.
---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on behalf
of the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
---
MIT License
Copyright (c) 2025 Anthropic, PBC
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +206,11 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Creative Commons Attribution 4.0 International (CC-BY-4.0)
Documentation in this project (excluding specifications) is licensed under
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.

View File

@@ -1643,7 +1643,7 @@ See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities.
## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](LICENSE) file for details.
## 💬 Community

78
package-lock.json generated
View File

@@ -7,7 +7,7 @@
"": {
"name": "@modelcontextprotocol/servers",
"version": "0.6.2",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"workspaces": [
"src/*"
],
@@ -481,9 +481,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.7",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -652,12 +652,12 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.7",
"@hono/node-server": "^1.19.9",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -665,14 +665,15 @@
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"jose": "^6.1.1",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"hono": "^4.11.4",
"jose": "^6.1.3",
"json-schema-typed": "^8.0.2",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.25 || ^4.0",
"zod-to-json-schema": "^3.25.0"
"zod-to-json-schema": "^3.25.1"
},
"engines": {
"node": ">=18"
@@ -1916,10 +1917,13 @@
}
},
"node_modules/express-rate-limit": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
},
"engines": {
"node": ">= 16"
},
@@ -1927,7 +1931,7 @@
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": "^4.11 || 5 || ^5.0.0-beta.1"
"express": ">= 4.11"
}
},
"node_modules/fast-deep-equal": {
@@ -2147,11 +2151,10 @@
}
},
"node_modules/hono": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz",
"integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==",
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -2226,6 +2229,15 @@
"node": ">= 0.10"
}
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -3693,9 +3705,9 @@
}
},
"node_modules/zod-to-json-schema": {
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.25 || ^4"
@@ -3779,9 +3791,9 @@
"src/everything": {
"name": "@modelcontextprotocol/server-everything",
"version": "2.0.0",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"cors": "^2.8.5",
"express": "^5.2.1",
"jszip": "^3.10.1",
@@ -3794,17 +3806,19 @@
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@vitest/coverage-v8": "^2.1.8",
"prettier": "^2.8.8",
"shx": "^0.3.4",
"typescript": "^5.6.2"
"typescript": "^5.6.2",
"vitest": "^2.1.8"
}
},
"src/filesystem": {
"name": "@modelcontextprotocol/server-filesystem",
"version": "0.6.3",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"diff": "^8.0.3",
"glob": "^10.5.0",
"minimatch": "^10.0.1",
@@ -3919,9 +3933,9 @@
"src/memory": {
"name": "@modelcontextprotocol/server-memory",
"version": "0.6.3",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
"@modelcontextprotocol/sdk": "^1.26.0"
},
"bin": {
"mcp-server-memory": "dist/index.js"
@@ -3991,9 +4005,9 @@
"src/sequentialthinking": {
"name": "@modelcontextprotocol/server-sequential-thinking",
"version": "0.6.2",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"chalk": "^5.3.0",
"yargs": "^17.7.2"
},

View File

@@ -3,8 +3,8 @@
"private": true,
"version": "0.6.2",
"description": "Model Context Protocol servers",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"license": "SEE LICENSE IN LICENSE",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"type": "module",

View File

@@ -0,0 +1,179 @@
import { describe, it, expect, vi } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerSimplePrompt } from '../prompts/simple.js';
import { registerArgumentsPrompt } from '../prompts/args.js';
import { registerPromptWithCompletions } from '../prompts/completions.js';
import { registerEmbeddedResourcePrompt } from '../prompts/resource.js';
// Helper to capture registered prompt handlers
function createMockServer() {
const handlers: Map<string, Function> = new Map();
const configs: Map<string, any> = new Map();
const mockServer = {
registerPrompt: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
configs.set(name, config);
}),
} as unknown as McpServer;
return { mockServer, handlers, configs };
}
describe('Prompts', () => {
describe('simple-prompt', () => {
it('should return fixed message with no arguments', () => {
const { mockServer, handlers } = createMockServer();
registerSimplePrompt(mockServer);
const handler = handlers.get('simple-prompt')!;
const result = handler();
expect(result).toEqual({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'This is a simple prompt without arguments.',
},
},
],
});
});
});
describe('args-prompt', () => {
it('should include city in message', () => {
const { mockServer, handlers } = createMockServer();
registerArgumentsPrompt(mockServer);
const handler = handlers.get('args-prompt')!;
const result = handler({ city: 'San Francisco' });
expect(result.messages[0].content.text).toBe("What's weather in San Francisco?");
});
it('should include city and state in message', () => {
const { mockServer, handlers } = createMockServer();
registerArgumentsPrompt(mockServer);
const handler = handlers.get('args-prompt')!;
const result = handler({ city: 'San Francisco', state: 'California' });
expect(result.messages[0].content.text).toBe(
"What's weather in San Francisco, California?"
);
});
it('should handle city only (optional state omitted)', () => {
const { mockServer, handlers } = createMockServer();
registerArgumentsPrompt(mockServer);
const handler = handlers.get('args-prompt')!;
const result = handler({ city: 'New York' });
expect(result.messages[0].content.text).toBe("What's weather in New York?");
expect(result.messages[0].content.text).not.toContain(',');
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content.type).toBe('text');
});
});
describe('completable-prompt', () => {
it('should generate promotion message with department and name', () => {
const { mockServer, handlers } = createMockServer();
registerPromptWithCompletions(mockServer);
const handler = handlers.get('completable-prompt')!;
const result = handler({ department: 'Engineering', name: 'Alice' });
expect(result.messages[0].content.text).toBe(
'Please promote Alice to the head of the Engineering team.'
);
});
it('should work with different departments', () => {
const { mockServer, handlers } = createMockServer();
registerPromptWithCompletions(mockServer);
const handler = handlers.get('completable-prompt')!;
const salesResult = handler({ department: 'Sales', name: 'David' });
expect(salesResult.messages[0].content.text).toContain('Sales');
expect(salesResult.messages[0].content.text).toContain('David');
expect(salesResult.messages[0].role).toBe('user');
const marketingResult = handler({ department: 'Marketing', name: 'Grace' });
expect(marketingResult.messages[0].content.text).toContain('Marketing');
expect(marketingResult.messages[0].content.text).toContain('Grace');
});
});
describe('resource-prompt', () => {
it('should return text resource reference', () => {
const { mockServer, handlers } = createMockServer();
registerEmbeddedResourcePrompt(mockServer);
const handler = handlers.get('resource-prompt')!;
const result = handler({ resourceType: 'Text', resourceId: '1' });
expect(result.messages).toHaveLength(2);
expect(result.messages[0].content.text).toContain('Text');
expect(result.messages[0].content.text).toContain('1');
expect(result.messages[1].content.type).toBe('resource');
expect(result.messages[1].content.resource.uri).toContain('text/1');
});
it('should return blob resource reference', () => {
const { mockServer, handlers } = createMockServer();
registerEmbeddedResourcePrompt(mockServer);
const handler = handlers.get('resource-prompt')!;
const result = handler({ resourceType: 'Blob', resourceId: '5' });
expect(result.messages[0].content.text).toContain('Blob');
expect(result.messages[1].content.resource.uri).toContain('blob/5');
});
it('should reject invalid resource type', () => {
const { mockServer, handlers } = createMockServer();
registerEmbeddedResourcePrompt(mockServer);
const handler = handlers.get('resource-prompt')!;
expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow(
'Invalid resourceType'
);
});
it('should reject invalid resource ID', () => {
const { mockServer, handlers } = createMockServer();
registerEmbeddedResourcePrompt(mockServer);
const handler = handlers.get('resource-prompt')!;
expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow(
'Invalid resourceId'
);
expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow(
'Invalid resourceId'
);
expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow(
'Invalid resourceId'
);
});
it('should include both intro text and resource messages', () => {
const { mockServer, handlers } = createMockServer();
registerEmbeddedResourcePrompt(mockServer);
const handler = handlers.get('resource-prompt')!;
const result = handler({ resourceType: 'Text', resourceId: '3' });
expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content.type).toBe('text');
expect(result.messages[1].role).toBe('user');
expect(result.messages[1].content.type).toBe('resource');
});
});
});

View File

@@ -0,0 +1,152 @@
import { describe, it, expect, vi } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
// Create mock server
function createMockServer() {
return {
registerTool: vi.fn(),
registerPrompt: vi.fn(),
registerResource: vi.fn(),
server: {
getClientCapabilities: vi.fn(() => ({})),
setRequestHandler: vi.fn(),
},
sendLoggingMessage: vi.fn(),
sendResourceUpdated: vi.fn(),
} as unknown as McpServer;
}
describe('Registration Index Files', () => {
describe('tools/index.ts', () => {
it('should register all standard tools', async () => {
const { registerTools } = await import('../tools/index.js');
const mockServer = createMockServer();
registerTools(mockServer);
// Should register 12 standard tools (non-conditional)
expect(mockServer.registerTool).toHaveBeenCalledTimes(12);
// Verify specific tools are registered
const registeredTools = (mockServer.registerTool as any).mock.calls.map(
(call: any[]) => call[0]
);
expect(registeredTools).toContain('echo');
expect(registeredTools).toContain('get-sum');
expect(registeredTools).toContain('get-env');
expect(registeredTools).toContain('get-tiny-image');
expect(registeredTools).toContain('get-structured-content');
expect(registeredTools).toContain('get-annotated-message');
expect(registeredTools).toContain('trigger-long-running-operation');
expect(registeredTools).toContain('get-resource-links');
expect(registeredTools).toContain('get-resource-reference');
expect(registeredTools).toContain('gzip-file-as-resource');
expect(registeredTools).toContain('toggle-simulated-logging');
expect(registeredTools).toContain('toggle-subscriber-updates');
});
it('should register conditional tools based on capabilities', async () => {
const { registerConditionalTools } = await import('../tools/index.js');
// Server with all capabilities including experimental tasks API
const mockServerWithCapabilities = {
registerTool: vi.fn(),
server: {
getClientCapabilities: vi.fn(() => ({
roots: {},
elicitation: {},
sampling: {},
})),
},
experimental: {
tasks: {
registerToolTask: vi.fn(),
},
},
} as unknown as McpServer;
registerConditionalTools(mockServerWithCapabilities);
// Should register 3 conditional tools + 3 task-based tools when all capabilities present
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(3);
const registeredTools = (
mockServerWithCapabilities.registerTool as any
).mock.calls.map((call: any[]) => call[0]);
expect(registeredTools).toContain('get-roots-list');
expect(registeredTools).toContain('trigger-elicitation-request');
expect(registeredTools).toContain('trigger-sampling-request');
// Task-based tools are registered via experimental.tasks.registerToolTask
expect(mockServerWithCapabilities.experimental.tasks.registerToolTask).toHaveBeenCalled();
});
it('should not register conditional tools when capabilities missing', async () => {
const { registerConditionalTools } = await import('../tools/index.js');
const mockServerNoCapabilities = {
registerTool: vi.fn(),
server: {
getClientCapabilities: vi.fn(() => ({})),
},
experimental: {
tasks: {
registerToolTask: vi.fn(),
},
},
} as unknown as McpServer;
registerConditionalTools(mockServerNoCapabilities);
// Should not register any capability-gated tools when capabilities are missing
expect(mockServerNoCapabilities.registerTool).not.toHaveBeenCalled();
});
});
describe('prompts/index.ts', () => {
it('should register all prompts', async () => {
const { registerPrompts } = await import('../prompts/index.js');
const mockServer = createMockServer();
registerPrompts(mockServer);
// Should register 4 prompts
expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4);
const registeredPrompts = (mockServer.registerPrompt as any).mock.calls.map(
(call: any[]) => call[0]
);
expect(registeredPrompts).toContain('simple-prompt');
expect(registeredPrompts).toContain('args-prompt');
expect(registeredPrompts).toContain('completable-prompt');
expect(registeredPrompts).toContain('resource-prompt');
});
});
describe('resources/index.ts', () => {
it('should register resource templates', async () => {
const { registerResources } = await import('../resources/index.js');
const mockServer = createMockServer();
registerResources(mockServer);
// Should register at least the 2 resource templates (text and blob) plus file resources
expect(mockServer.registerResource).toHaveBeenCalled();
const registeredResources = (mockServer.registerResource as any).mock.calls.map(
(call: any[]) => call[0]
);
expect(registeredResources).toContain('Dynamic Text Resource');
expect(registeredResources).toContain('Dynamic Blob Resource');
});
it('should read instructions from file', async () => {
const { readInstructions } = await import('../resources/index.js');
const instructions = readInstructions();
// Should return a string (either content or error message)
expect(typeof instructions).toBe('string');
expect(instructions.length).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,327 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
textResource,
blobResource,
textResourceUri,
blobResourceUri,
RESOURCE_TYPE_TEXT,
RESOURCE_TYPE_BLOB,
RESOURCE_TYPES,
resourceTypeCompleter,
resourceIdForPromptCompleter,
resourceIdForResourceTemplateCompleter,
registerResourceTemplates,
} from '../resources/templates.js';
import {
getSessionResourceURI,
registerSessionResource,
} from '../resources/session.js';
import { registerFileResources } from '../resources/files.js';
import {
setSubscriptionHandlers,
beginSimulatedResourceUpdates,
stopSimulatedResourceUpdates,
} from '../resources/subscriptions.js';
describe('Resource Templates', () => {
describe('Constants', () => {
it('should include both types in RESOURCE_TYPES array', () => {
expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT);
expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB);
expect(RESOURCE_TYPES).toHaveLength(2);
});
});
describe('textResourceUri', () => {
it('should create URL for text resource', () => {
const uri = textResourceUri(1);
expect(uri.toString()).toBe('demo://resource/dynamic/text/1');
});
it('should handle different resource IDs', () => {
expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5');
expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100');
});
});
describe('blobResourceUri', () => {
it('should create URL for blob resource', () => {
const uri = blobResourceUri(1);
expect(uri.toString()).toBe('demo://resource/dynamic/blob/1');
});
it('should handle different resource IDs', () => {
expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5');
expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100');
});
});
describe('textResource', () => {
it('should create text resource with correct structure', () => {
const uri = textResourceUri(1);
const resource = textResource(uri, 1);
expect(resource.uri).toBe(uri.toString());
expect(resource.mimeType).toBe('text/plain');
expect(resource.text).toContain('Resource 1');
expect(resource.text).toContain('plaintext');
});
it('should include timestamp in content', () => {
const uri = textResourceUri(2);
const resource = textResource(uri, 2);
// Timestamp format varies, just check it contains time-related content
expect(resource.text).toMatch(/\d/);
});
});
describe('blobResource', () => {
it('should create blob resource with correct structure', () => {
const uri = blobResourceUri(1);
const resource = blobResource(uri, 1);
expect(resource.uri).toBe(uri.toString());
expect(resource.mimeType).toBe('text/plain');
expect(resource.blob).toBeDefined();
});
it('should create valid base64 encoded content', () => {
const uri = blobResourceUri(3);
const resource = blobResource(uri, 3);
// Decode and verify content
const decoded = Buffer.from(resource.blob, 'base64').toString();
expect(decoded).toContain('Resource 3');
expect(decoded).toContain('base64 blob');
});
});
describe('resourceTypeCompleter', () => {
it('should be defined as a completable schema', () => {
// The completer is a zod schema wrapped with completable
expect(resourceTypeCompleter).toBeDefined();
// It should have the zod parse method
expect(typeof (resourceTypeCompleter as any).parse).toBe('function');
});
it('should validate string resource types', () => {
// Test that valid strings pass validation
expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow();
expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow();
});
});
describe('resourceIdForPromptCompleter', () => {
it('should be defined as a completable schema', () => {
expect(resourceIdForPromptCompleter).toBeDefined();
expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function');
});
it('should validate string IDs', () => {
// Test that valid strings pass validation
expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow();
expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow();
});
});
describe('resourceIdForResourceTemplateCompleter', () => {
it('should validate positive integer IDs', () => {
expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']);
expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']);
});
it('should reject invalid IDs', () => {
expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]);
expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]);
expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]);
});
});
describe('registerResourceTemplates', () => {
it('should register text and blob resource templates', () => {
const registeredResources: any[] = [];
const mockServer = {
registerResource: vi.fn((...args) => {
registeredResources.push(args);
}),
} as unknown as McpServer;
registerResourceTemplates(mockServer);
expect(mockServer.registerResource).toHaveBeenCalledTimes(2);
// Check text resource registration
const textRegistration = registeredResources.find((r) =>
r[0].includes('Text')
);
expect(textRegistration).toBeDefined();
expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate);
// Check blob resource registration
const blobRegistration = registeredResources.find((r) =>
r[0].includes('Blob')
);
expect(blobRegistration).toBeDefined();
});
});
});
describe('Session Resources', () => {
describe('getSessionResourceURI', () => {
it('should generate correct URI for resource name', () => {
expect(getSessionResourceURI('test')).toBe('demo://resource/session/test');
});
it('should handle various resource names', () => {
expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file');
expect(getSessionResourceURI('document_123')).toBe(
'demo://resource/session/document_123'
);
});
});
describe('registerSessionResource', () => {
it('should register text resource and return resource link', () => {
const registrations: any[] = [];
const mockServer = {
registerResource: vi.fn((...args) => {
registrations.push(args);
}),
} as unknown as McpServer;
const resource = {
uri: 'demo://resource/session/test-file',
name: 'test-file',
mimeType: 'text/plain',
description: 'A test file',
};
const result = registerSessionResource(
mockServer,
resource,
'text',
'Hello, World!'
);
expect(result.type).toBe('resource_link');
expect(result.uri).toBe(resource.uri);
expect(result.name).toBe(resource.name);
expect(mockServer.registerResource).toHaveBeenCalledWith(
'test-file',
'demo://resource/session/test-file',
expect.objectContaining({
mimeType: 'text/plain',
description: 'A test file',
}),
expect.any(Function)
);
});
it('should register blob resource correctly', () => {
const mockServer = {
registerResource: vi.fn(),
} as unknown as McpServer;
const resource = {
uri: 'demo://resource/session/binary-file',
name: 'binary-file',
mimeType: 'application/octet-stream',
};
const blobContent = Buffer.from('binary data').toString('base64');
const result = registerSessionResource(mockServer, resource, 'blob', blobContent);
expect(result.type).toBe('resource_link');
expect(mockServer.registerResource).toHaveBeenCalled();
});
it('should return resource handler that provides correct content', async () => {
let capturedHandler: Function | null = null;
const mockServer = {
registerResource: vi.fn((_name, _uri, _config, handler) => {
capturedHandler = handler;
}),
} as unknown as McpServer;
const resource = {
uri: 'demo://resource/session/content-test',
name: 'content-test',
mimeType: 'text/plain',
};
registerSessionResource(mockServer, resource, 'text', 'Test content here');
expect(capturedHandler).not.toBeNull();
const handlerResult = await capturedHandler!(new URL(resource.uri));
expect(handlerResult.contents).toHaveLength(1);
expect(handlerResult.contents[0].text).toBe('Test content here');
expect(handlerResult.contents[0].mimeType).toBe('text/plain');
});
});
});
describe('File Resources', () => {
describe('registerFileResources', () => {
it('should register file resources when docs directory exists', () => {
const mockServer = {
registerResource: vi.fn(),
} as unknown as McpServer;
registerFileResources(mockServer);
// The docs folder exists in the everything server and contains files
// so registerResource should have been called
expect(mockServer.registerResource).toHaveBeenCalled();
});
});
});
describe('Subscriptions', () => {
describe('setSubscriptionHandlers', () => {
it('should set request handlers on server', () => {
const mockServer = {
server: {
setRequestHandler: vi.fn(),
},
sendLoggingMessage: vi.fn(),
} as unknown as McpServer;
setSubscriptionHandlers(mockServer);
// Should set both subscribe and unsubscribe handlers
expect(mockServer.server.setRequestHandler).toHaveBeenCalledTimes(2);
});
});
describe('simulated resource updates lifecycle', () => {
afterEach(() => {
// Clean up any intervals
stopSimulatedResourceUpdates('lifecycle-test-session');
});
it('should start and stop updates without errors', () => {
const mockServer = {
server: {
notification: vi.fn(),
},
} as unknown as McpServer;
// Start updates - should work for both defined and undefined sessionId
beginSimulatedResourceUpdates(mockServer, 'lifecycle-test-session');
beginSimulatedResourceUpdates(mockServer, undefined);
// Stop updates - should handle all cases gracefully
stopSimulatedResourceUpdates('lifecycle-test-session');
stopSimulatedResourceUpdates('non-existent-session');
stopSimulatedResourceUpdates(undefined);
// If we got here without throwing, the lifecycle works correctly
expect(true).toBe(true);
});
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect, vi } from 'vitest';
import { createServer } from '../server/index.js';
describe('Server Factory', () => {
describe('createServer', () => {
it('should return a ServerFactoryResponse object', () => {
const result = createServer();
expect(result).toHaveProperty('server');
expect(result).toHaveProperty('cleanup');
});
it('should return a cleanup function', () => {
const { cleanup } = createServer();
expect(typeof cleanup).toBe('function');
});
it('should create an McpServer instance', () => {
const { server } = createServer();
expect(server).toBeDefined();
expect(server.server).toBeDefined();
});
it('should have an oninitialized handler set', () => {
const { server } = createServer();
expect(server.server.oninitialized).toBeDefined();
});
it('should allow multiple servers to be created', () => {
const result1 = createServer();
const result2 = createServer();
expect(result1.server).toBeDefined();
expect(result2.server).toBeDefined();
expect(result1.server).not.toBe(result2.server);
});
});
});

View File

@@ -0,0 +1,820 @@
import { describe, it, expect, vi } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerEchoTool, EchoSchema } from '../tools/echo.js';
import { registerGetSumTool } from '../tools/get-sum.js';
import { registerGetEnvTool } from '../tools/get-env.js';
import { registerGetTinyImageTool, MCP_TINY_IMAGE } from '../tools/get-tiny-image.js';
import { registerGetStructuredContentTool } from '../tools/get-structured-content.js';
import { registerGetAnnotatedMessageTool } from '../tools/get-annotated-message.js';
import { registerTriggerLongRunningOperationTool } from '../tools/trigger-long-running-operation.js';
import { registerGetResourceLinksTool } from '../tools/get-resource-links.js';
import { registerGetResourceReferenceTool } from '../tools/get-resource-reference.js';
import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-logging.js';
import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js';
import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js';
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
// Helper to capture registered tool handlers
function createMockServer() {
const handlers: Map<string, Function> = new Map();
const configs: Map<string, any> = new Map();
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
configs.set(name, config);
}),
server: {
getClientCapabilities: vi.fn(() => ({})),
notification: vi.fn(),
},
sendLoggingMessage: vi.fn(),
sendResourceUpdated: vi.fn(),
} as unknown as McpServer;
return { mockServer, handlers, configs };
}
describe('Tools', () => {
describe('echo', () => {
it('should echo back the message', async () => {
const { mockServer, handlers } = createMockServer();
registerEchoTool(mockServer);
const handler = handlers.get('echo')!;
const result = await handler({ message: 'Hello, World!' });
expect(result).toEqual({
content: [{ type: 'text', text: 'Echo: Hello, World!' }],
});
});
it('should handle empty message', async () => {
const { mockServer, handlers } = createMockServer();
registerEchoTool(mockServer);
const handler = handlers.get('echo')!;
const result = await handler({ message: '' });
expect(result).toEqual({
content: [{ type: 'text', text: 'Echo: ' }],
});
});
it('should reject invalid input', async () => {
const { mockServer, handlers } = createMockServer();
registerEchoTool(mockServer);
const handler = handlers.get('echo')!;
await expect(handler({})).rejects.toThrow();
await expect(handler({ message: 123 })).rejects.toThrow();
});
});
describe('EchoSchema', () => {
it('should validate correct input', () => {
const result = EchoSchema.parse({ message: 'test' });
expect(result).toEqual({ message: 'test' });
});
it('should reject missing message', () => {
expect(() => EchoSchema.parse({})).toThrow();
});
it('should reject non-string message', () => {
expect(() => EchoSchema.parse({ message: 123 })).toThrow();
});
});
describe('get-sum', () => {
it('should calculate sum of two positive numbers', async () => {
const { mockServer, handlers } = createMockServer();
registerGetSumTool(mockServer);
const handler = handlers.get('get-sum')!;
const result = await handler({ a: 5, b: 3 });
expect(result).toEqual({
content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }],
});
});
it('should calculate sum with negative numbers', async () => {
const { mockServer, handlers } = createMockServer();
registerGetSumTool(mockServer);
const handler = handlers.get('get-sum')!;
const result = await handler({ a: -5, b: 3 });
expect(result).toEqual({
content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }],
});
});
it('should calculate sum with zero', async () => {
const { mockServer, handlers } = createMockServer();
registerGetSumTool(mockServer);
const handler = handlers.get('get-sum')!;
const result = await handler({ a: 0, b: 0 });
expect(result).toEqual({
content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }],
});
});
it('should handle floating point numbers', async () => {
const { mockServer, handlers } = createMockServer();
registerGetSumTool(mockServer);
const handler = handlers.get('get-sum')!;
const result = await handler({ a: 1.5, b: 2.5 });
expect(result).toEqual({
content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }],
});
});
it('should reject invalid input', async () => {
const { mockServer, handlers } = createMockServer();
registerGetSumTool(mockServer);
const handler = handlers.get('get-sum')!;
await expect(handler({})).rejects.toThrow();
await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow();
await expect(handler({ a: 5 })).rejects.toThrow();
});
});
describe('get-env', () => {
it('should return all environment variables as JSON', async () => {
const { mockServer, handlers } = createMockServer();
registerGetEnvTool(mockServer);
const handler = handlers.get('get-env')!;
process.env.TEST_VAR_EVERYTHING = 'test_value';
const result = await handler({});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const envJson = JSON.parse(result.content[0].text);
expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value');
delete process.env.TEST_VAR_EVERYTHING;
});
it('should return valid JSON', async () => {
const { mockServer, handlers } = createMockServer();
registerGetEnvTool(mockServer);
const handler = handlers.get('get-env')!;
const result = await handler({});
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
});
});
describe('get-tiny-image', () => {
it('should return image content with text descriptions', async () => {
const { mockServer, handlers } = createMockServer();
registerGetTinyImageTool(mockServer);
const handler = handlers.get('get-tiny-image')!;
const result = await handler({});
expect(result.content).toHaveLength(3);
expect(result.content[0]).toEqual({
type: 'text',
text: "Here's the image you requested:",
});
expect(result.content[1]).toEqual({
type: 'image',
data: MCP_TINY_IMAGE,
mimeType: 'image/png',
});
expect(result.content[2]).toEqual({
type: 'text',
text: 'The image above is the MCP logo.',
});
});
it('should return valid base64 image data', async () => {
const { mockServer, handlers } = createMockServer();
registerGetTinyImageTool(mockServer);
const handler = handlers.get('get-tiny-image')!;
const result = await handler({});
const imageContent = result.content[1];
expect(imageContent.type).toBe('image');
expect(imageContent.mimeType).toBe('image/png');
// Verify it's valid base64
expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow();
});
});
describe('get-structured-content', () => {
it('should return weather for New York', async () => {
const { mockServer, handlers } = createMockServer();
registerGetStructuredContentTool(mockServer);
const handler = handlers.get('get-structured-content')!;
const result = await handler({ location: 'New York' });
expect(result.structuredContent).toEqual({
temperature: 33,
conditions: 'Cloudy',
humidity: 82,
});
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent);
});
it('should return weather for Chicago', async () => {
const { mockServer, handlers } = createMockServer();
registerGetStructuredContentTool(mockServer);
const handler = handlers.get('get-structured-content')!;
const result = await handler({ location: 'Chicago' });
expect(result.structuredContent).toEqual({
temperature: 36,
conditions: 'Light rain / drizzle',
humidity: 82,
});
});
it('should return weather for Los Angeles', async () => {
const { mockServer, handlers } = createMockServer();
registerGetStructuredContentTool(mockServer);
const handler = handlers.get('get-structured-content')!;
const result = await handler({ location: 'Los Angeles' });
expect(result.structuredContent).toEqual({
temperature: 73,
conditions: 'Sunny / Clear',
humidity: 48,
});
});
});
describe('get-annotated-message', () => {
it('should return error message with high priority', async () => {
const { mockServer, handlers } = createMockServer();
registerGetAnnotatedMessageTool(mockServer);
const handler = handlers.get('get-annotated-message')!;
const result = await handler({ messageType: 'error', includeImage: false });
expect(result.content).toHaveLength(1);
expect(result.content[0].text).toBe('Error: Operation failed');
expect(result.content[0].annotations).toEqual({
priority: 1.0,
audience: ['user', 'assistant'],
});
});
it('should return success message with medium priority', async () => {
const { mockServer, handlers } = createMockServer();
registerGetAnnotatedMessageTool(mockServer);
const handler = handlers.get('get-annotated-message')!;
const result = await handler({ messageType: 'success', includeImage: false });
expect(result.content[0].text).toBe('Operation completed successfully');
expect(result.content[0].annotations.priority).toBe(0.7);
expect(result.content[0].annotations.audience).toEqual(['user']);
});
it('should return debug message with low priority', async () => {
const { mockServer, handlers } = createMockServer();
registerGetAnnotatedMessageTool(mockServer);
const handler = handlers.get('get-annotated-message')!;
const result = await handler({ messageType: 'debug', includeImage: false });
expect(result.content[0].text).toContain('Debug:');
expect(result.content[0].annotations.priority).toBe(0.3);
expect(result.content[0].annotations.audience).toEqual(['assistant']);
});
it('should include annotated image when requested', async () => {
const { mockServer, handlers } = createMockServer();
registerGetAnnotatedMessageTool(mockServer);
const handler = handlers.get('get-annotated-message')!;
const result = await handler({ messageType: 'success', includeImage: true });
expect(result.content).toHaveLength(2);
expect(result.content[1].type).toBe('image');
expect(result.content[1].annotations).toEqual({
priority: 0.5,
audience: ['user'],
});
});
});
describe('trigger-long-running-operation', () => {
it('should complete operation and return result', async () => {
const { mockServer, handlers } = createMockServer();
registerTriggerLongRunningOperationTool(mockServer);
const handler = handlers.get('trigger-long-running-operation')!;
// Use very short duration for test
const result = await handler(
{ duration: 0.1, steps: 2 },
{ _meta: {}, requestId: 'test-123' }
);
expect(result.content[0].text).toContain('Long running operation completed');
expect(result.content[0].text).toContain('Duration: 0.1 seconds');
expect(result.content[0].text).toContain('Steps: 2');
}, 10000);
it('should send progress notifications when progressToken provided', async () => {
const { mockServer, handlers } = createMockServer();
registerTriggerLongRunningOperationTool(mockServer);
const handler = handlers.get('trigger-long-running-operation')!;
await handler(
{ duration: 0.1, steps: 2 },
{ _meta: { progressToken: 'token-123' }, requestId: 'test-456', sessionId: 'session-1' }
);
expect(mockServer.server.notification).toHaveBeenCalledTimes(2);
expect(mockServer.server.notification).toHaveBeenCalledWith(
expect.objectContaining({
method: 'notifications/progress',
params: expect.objectContaining({
progressToken: 'token-123',
}),
}),
expect.any(Object)
);
}, 10000);
});
describe('get-resource-links', () => {
it('should return specified number of resource links', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceLinksTool(mockServer);
const handler = handlers.get('get-resource-links')!;
const result = await handler({ count: 3 });
// 1 intro text + 3 resource links
expect(result.content).toHaveLength(4);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('3 resource links');
// Check resource links
for (let i = 1; i < 4; i++) {
expect(result.content[i].type).toBe('resource_link');
expect(result.content[i].uri).toBeDefined();
expect(result.content[i].name).toBeDefined();
}
});
it('should alternate between text and blob resources', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceLinksTool(mockServer);
const handler = handlers.get('get-resource-links')!;
const result = await handler({ count: 4 });
// Odd IDs (1, 3) are blob, even IDs (2, 4) are text
expect(result.content[1].name).toContain('Blob');
expect(result.content[2].name).toContain('Text');
expect(result.content[3].name).toContain('Blob');
expect(result.content[4].name).toContain('Text');
});
it('should use default count of 3', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceLinksTool(mockServer);
const handler = handlers.get('get-resource-links')!;
const result = await handler({});
// 1 intro text + 3 resource links (default)
expect(result.content).toHaveLength(4);
});
});
describe('get-resource-reference', () => {
it('should return text resource reference', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceReferenceTool(mockServer);
const handler = handlers.get('get-resource-reference')!;
const result = await handler({ resourceType: 'Text', resourceId: 1 });
expect(result.content).toHaveLength(3);
expect(result.content[0].text).toContain('Resource 1');
expect(result.content[1].type).toBe('resource');
expect(result.content[1].resource.uri).toContain('text/1');
expect(result.content[2].text).toContain('URI');
});
it('should return blob resource reference', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceReferenceTool(mockServer);
const handler = handlers.get('get-resource-reference')!;
const result = await handler({ resourceType: 'Blob', resourceId: 5 });
expect(result.content[1].resource.uri).toContain('blob/5');
});
it('should reject invalid resource type', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceReferenceTool(mockServer);
const handler = handlers.get('get-resource-reference')!;
await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow(
'Invalid resourceType'
);
});
it('should reject invalid resource ID', async () => {
const { mockServer, handlers } = createMockServer();
registerGetResourceReferenceTool(mockServer);
const handler = handlers.get('get-resource-reference')!;
await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow(
'Invalid resourceId'
);
await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow(
'Invalid resourceId'
);
await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow(
'Invalid resourceId'
);
});
});
describe('toggle-simulated-logging', () => {
it('should start logging when not active', async () => {
const { mockServer, handlers } = createMockServer();
registerToggleSimulatedLoggingTool(mockServer);
const handler = handlers.get('toggle-simulated-logging')!;
const result = await handler({}, { sessionId: 'test-session-1' });
expect(result.content[0].text).toContain('Started');
expect(result.content[0].text).toContain('test-session-1');
});
it('should stop logging when already active', async () => {
const { mockServer, handlers } = createMockServer();
registerToggleSimulatedLoggingTool(mockServer);
const handler = handlers.get('toggle-simulated-logging')!;
// First call starts logging
await handler({}, { sessionId: 'test-session-2' });
// Second call stops logging
const result = await handler({}, { sessionId: 'test-session-2' });
expect(result.content[0].text).toContain('Stopped');
expect(result.content[0].text).toContain('test-session-2');
});
it('should handle undefined sessionId', async () => {
const { mockServer, handlers } = createMockServer();
registerToggleSimulatedLoggingTool(mockServer);
const handler = handlers.get('toggle-simulated-logging')!;
const result = await handler({}, {});
expect(result.content[0].text).toContain('Started');
});
});
describe('toggle-subscriber-updates', () => {
it('should start updates when not active', async () => {
const { mockServer, handlers } = createMockServer();
registerToggleSubscriberUpdatesTool(mockServer);
const handler = handlers.get('toggle-subscriber-updates')!;
const result = await handler({}, { sessionId: 'sub-session-1' });
expect(result.content[0].text).toContain('Started');
expect(result.content[0].text).toContain('sub-session-1');
});
it('should stop updates when already active', async () => {
const { mockServer, handlers } = createMockServer();
registerToggleSubscriberUpdatesTool(mockServer);
const handler = handlers.get('toggle-subscriber-updates')!;
// First call starts updates
await handler({}, { sessionId: 'sub-session-2' });
// Second call stops updates
const result = await handler({}, { sessionId: 'sub-session-2' });
expect(result.content[0].text).toContain('Stopped');
expect(result.content[0].text).toContain('sub-session-2');
});
});
describe('trigger-sampling-request', () => {
it('should not register when client does not support sampling', () => {
const { mockServer } = createMockServer();
registerTriggerSamplingRequestTool(mockServer);
// Tool should not be registered since mock server returns empty capabilities
expect(mockServer.registerTool).not.toHaveBeenCalled();
});
it('should register when client supports sampling', () => {
const handlers: Map<string, Function> = new Map();
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ sampling: {} })),
},
} as unknown as McpServer;
registerTriggerSamplingRequestTool(mockServer);
expect(mockServer.registerTool).toHaveBeenCalledWith(
'trigger-sampling-request',
expect.objectContaining({
title: 'Trigger Sampling Request Tool',
description: expect.stringContaining('Sampling'),
}),
expect.any(Function)
);
});
it('should send sampling request and return result', async () => {
const handlers: Map<string, Function> = new Map();
const mockSendRequest = vi.fn().mockResolvedValue({
model: 'test-model',
content: { type: 'text', text: 'LLM response' },
});
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ sampling: {} })),
},
} as unknown as McpServer;
registerTriggerSamplingRequestTool(mockServer);
const handler = handlers.get('trigger-sampling-request')!;
const result = await handler(
{ prompt: 'Test prompt', maxTokens: 50 },
{ sendRequest: mockSendRequest }
);
expect(mockSendRequest).toHaveBeenCalledWith(
expect.objectContaining({
method: 'sampling/createMessage',
params: expect.objectContaining({
maxTokens: 50,
}),
}),
expect.anything()
);
expect(result.content[0].text).toContain('LLM sampling result');
});
});
describe('trigger-elicitation-request', () => {
it('should not register when client does not support elicitation', () => {
const { mockServer } = createMockServer();
registerTriggerElicitationRequestTool(mockServer);
expect(mockServer.registerTool).not.toHaveBeenCalled();
});
it('should register when client supports elicitation', () => {
const handlers: Map<string, Function> = new Map();
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
},
} as unknown as McpServer;
registerTriggerElicitationRequestTool(mockServer);
expect(mockServer.registerTool).toHaveBeenCalledWith(
'trigger-elicitation-request',
expect.objectContaining({
title: 'Trigger Elicitation Request Tool',
description: expect.stringContaining('Elicitation'),
}),
expect.any(Function)
);
});
it('should handle accept action with user content', async () => {
const handlers: Map<string, Function> = new Map();
const mockSendRequest = vi.fn().mockResolvedValue({
action: 'accept',
content: {
name: 'John Doe',
check: true,
email: 'john@example.com',
},
});
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
},
} as unknown as McpServer;
registerTriggerElicitationRequestTool(mockServer);
const handler = handlers.get('trigger-elicitation-request')!;
const result = await handler({}, { sendRequest: mockSendRequest });
expect(result.content[0].text).toContain('✅');
expect(result.content[0].text).toContain('provided');
expect(result.content[1].text).toContain('John Doe');
});
it('should handle decline action', async () => {
const handlers: Map<string, Function> = new Map();
const mockSendRequest = vi.fn().mockResolvedValue({
action: 'decline',
});
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
},
} as unknown as McpServer;
registerTriggerElicitationRequestTool(mockServer);
const handler = handlers.get('trigger-elicitation-request')!;
const result = await handler({}, { sendRequest: mockSendRequest });
expect(result.content[0].text).toContain('❌');
expect(result.content[0].text).toContain('declined');
});
it('should handle cancel action', async () => {
const handlers: Map<string, Function> = new Map();
const mockSendRequest = vi.fn().mockResolvedValue({
action: 'cancel',
});
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
},
} as unknown as McpServer;
registerTriggerElicitationRequestTool(mockServer);
const handler = handlers.get('trigger-elicitation-request')!;
const result = await handler({}, { sendRequest: mockSendRequest });
expect(result.content[0].text).toContain('⚠️');
expect(result.content[0].text).toContain('cancelled');
});
});
describe('get-roots-list', () => {
it('should not register when client does not support roots', () => {
const { mockServer } = createMockServer();
registerGetRootsListTool(mockServer);
expect(mockServer.registerTool).not.toHaveBeenCalled();
});
it('should register when client supports roots', () => {
const handlers: Map<string, Function> = new Map();
const mockServer = {
registerTool: vi.fn((name: string, config: any, handler: Function) => {
handlers.set(name, handler);
}),
server: {
getClientCapabilities: vi.fn(() => ({ roots: {} })),
},
} as unknown as McpServer;
registerGetRootsListTool(mockServer);
expect(mockServer.registerTool).toHaveBeenCalledWith(
'get-roots-list',
expect.objectContaining({
title: 'Get Roots List Tool',
description: expect.stringContaining('roots'),
}),
expect.any(Function)
);
});
});
describe('gzip-file-as-resource', () => {
it('should compress data URI and return resource link', async () => {
const registeredResources: any[] = [];
const mockServer = {
registerTool: vi.fn(),
registerResource: vi.fn((...args) => {
registeredResources.push(args);
}),
} as unknown as McpServer;
// Get the handler
let handler: Function | null = null;
(mockServer.registerTool as any).mockImplementation(
(name: string, config: any, h: Function) => {
handler = h;
}
);
registerGZipFileAsResourceTool(mockServer);
// Create a data URI with test content
const testContent = 'Hello, World!';
const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`;
const result = await handler!(
{ name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' }
);
expect(result.content[0].type).toBe('resource_link');
expect(result.content[0].uri).toContain('test.txt.gz');
});
it('should return resource directly when outputType is resource', async () => {
const mockServer = {
registerTool: vi.fn(),
registerResource: vi.fn(),
} as unknown as McpServer;
let handler: Function | null = null;
(mockServer.registerTool as any).mockImplementation(
(name: string, config: any, h: Function) => {
handler = h;
}
);
registerGZipFileAsResourceTool(mockServer);
const testContent = 'Test content for compression';
const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`;
const result = await handler!(
{ name: 'output.gz', data: dataUri, outputType: 'resource' }
);
expect(result.content[0].type).toBe('resource');
expect(result.content[0].resource.mimeType).toBe('application/gzip');
expect(result.content[0].resource.blob).toBeDefined();
});
it('should reject unsupported URL protocols', async () => {
const mockServer = {
registerTool: vi.fn(),
registerResource: vi.fn(),
} as unknown as McpServer;
let handler: Function | null = null;
(mockServer.registerTool as any).mockImplementation(
(name: string, config: any, h: Function) => {
handler = h;
}
);
registerGZipFileAsResourceTool(mockServer);
await expect(
handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' })
).rejects.toThrow('Unsupported URL protocol');
});
});
});

View File

@@ -2,9 +2,9 @@
"name": "@modelcontextprotocol/server-everything",
"version": "2.0.0",
"description": "MCP server that exercises all the features of the MCP protocol",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-everything",
"author": "Anthropic, PBC (https://anthropic.com)",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"repository": {
@@ -26,10 +26,11 @@
"start:sse": "node dist/index.js sse",
"start:streamableHttp": "node dist/index.js streamableHttp",
"prettier:fix": "prettier --write .",
"prettier:check": "prettier --check ."
"prettier:check": "prettier --check .",
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"cors": "^2.8.5",
"express": "^5.2.1",
"jszip": "^3.10.1",
@@ -39,8 +40,10 @@
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@vitest/coverage-v8": "^2.1.8",
"shx": "^0.3.4",
"typescript": "^5.6.2",
"prettier": "^2.8.8"
"prettier": "^2.8.8",
"vitest": "^2.1.8"
}
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/__tests__/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['**/*.ts'],
exclude: ['**/__tests__/**', '**/dist/**'],
},
},
});

View File

@@ -33,4 +33,8 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3"]
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3", "pytest>=8.0.0", "pytest-asyncio>=0.21.0"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

View File

View File

@@ -0,0 +1,326 @@
"""Tests for the fetch MCP server."""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from mcp.shared.exceptions import McpError
from mcp_server_fetch.server import (
extract_content_from_html,
get_robots_txt_url,
check_may_autonomously_fetch_url,
fetch_url,
DEFAULT_USER_AGENT_AUTONOMOUS,
)
class TestGetRobotsTxtUrl:
"""Tests for get_robots_txt_url function."""
def test_simple_url(self):
"""Test with a simple URL."""
result = get_robots_txt_url("https://example.com/page")
assert result == "https://example.com/robots.txt"
def test_url_with_path(self):
"""Test with URL containing path."""
result = get_robots_txt_url("https://example.com/some/deep/path/page.html")
assert result == "https://example.com/robots.txt"
def test_url_with_query_params(self):
"""Test with URL containing query parameters."""
result = get_robots_txt_url("https://example.com/page?foo=bar&baz=qux")
assert result == "https://example.com/robots.txt"
def test_url_with_port(self):
"""Test with URL containing port number."""
result = get_robots_txt_url("https://example.com:8080/page")
assert result == "https://example.com:8080/robots.txt"
def test_url_with_fragment(self):
"""Test with URL containing fragment."""
result = get_robots_txt_url("https://example.com/page#section")
assert result == "https://example.com/robots.txt"
def test_http_url(self):
"""Test with HTTP URL."""
result = get_robots_txt_url("http://example.com/page")
assert result == "http://example.com/robots.txt"
class TestExtractContentFromHtml:
"""Tests for extract_content_from_html function."""
def test_simple_html(self):
"""Test with simple HTML content."""
html = """
<html>
<head><title>Test Page</title></head>
<body>
<article>
<h1>Hello World</h1>
<p>This is a test paragraph.</p>
</article>
</body>
</html>
"""
result = extract_content_from_html(html)
# readabilipy may extract different parts depending on the content
assert "test paragraph" in result
def test_html_with_links(self):
"""Test that links are converted to markdown."""
html = """
<html>
<body>
<article>
<p>Visit <a href="https://example.com">Example</a> for more.</p>
</article>
</body>
</html>
"""
result = extract_content_from_html(html)
assert "Example" in result
def test_empty_content_returns_error(self):
"""Test that empty/invalid HTML returns error message."""
html = ""
result = extract_content_from_html(html)
assert "<error>" in result
class TestCheckMayAutonomouslyFetchUrl:
"""Tests for check_may_autonomously_fetch_url function."""
@pytest.mark.asyncio
async def test_allows_when_robots_txt_404(self):
"""Test that fetching is allowed when robots.txt returns 404."""
mock_response = MagicMock()
mock_response.status_code = 404
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
# Should not raise
await check_may_autonomously_fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_blocks_when_robots_txt_401(self):
"""Test that fetching is blocked when robots.txt returns 401."""
mock_response = MagicMock()
mock_response.status_code = 401
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
with pytest.raises(McpError):
await check_may_autonomously_fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_blocks_when_robots_txt_403(self):
"""Test that fetching is blocked when robots.txt returns 403."""
mock_response = MagicMock()
mock_response.status_code = 403
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
with pytest.raises(McpError):
await check_may_autonomously_fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_allows_when_robots_txt_allows_all(self):
"""Test that fetching is allowed when robots.txt allows all."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = "User-agent: *\nAllow: /"
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
# Should not raise
await check_may_autonomously_fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_blocks_when_robots_txt_disallows_all(self):
"""Test that fetching is blocked when robots.txt disallows all."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = "User-agent: *\nDisallow: /"
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
with pytest.raises(McpError):
await check_may_autonomously_fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
class TestFetchUrl:
"""Tests for fetch_url function."""
@pytest.mark.asyncio
async def test_fetch_html_page(self):
"""Test fetching an HTML page returns markdown content."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = """
<html>
<body>
<article>
<h1>Test Page</h1>
<p>Hello World</p>
</article>
</body>
</html>
"""
mock_response.headers = {"content-type": "text/html"}
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
content, prefix = await fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS
)
# HTML is processed, so we check it returns something
assert isinstance(content, str)
assert prefix == ""
@pytest.mark.asyncio
async def test_fetch_html_page_raw(self):
"""Test fetching an HTML page with raw=True returns original HTML."""
html_content = "<html><body><h1>Test</h1></body></html>"
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = html_content
mock_response.headers = {"content-type": "text/html"}
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
content, prefix = await fetch_url(
"https://example.com/page",
DEFAULT_USER_AGENT_AUTONOMOUS,
force_raw=True
)
assert content == html_content
assert "cannot be simplified" in prefix
@pytest.mark.asyncio
async def test_fetch_json_returns_raw(self):
"""Test fetching JSON content returns raw content."""
json_content = '{"key": "value"}'
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = json_content
mock_response.headers = {"content-type": "application/json"}
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
content, prefix = await fetch_url(
"https://api.example.com/data",
DEFAULT_USER_AGENT_AUTONOMOUS
)
assert content == json_content
assert "cannot be simplified" in prefix
@pytest.mark.asyncio
async def test_fetch_404_raises_error(self):
"""Test that 404 response raises McpError."""
mock_response = MagicMock()
mock_response.status_code = 404
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
with pytest.raises(McpError):
await fetch_url(
"https://example.com/notfound",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_fetch_500_raises_error(self):
"""Test that 500 response raises McpError."""
mock_response = MagicMock()
mock_response.status_code = 500
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
with pytest.raises(McpError):
await fetch_url(
"https://example.com/error",
DEFAULT_USER_AGENT_AUTONOMOUS
)
@pytest.mark.asyncio
async def test_fetch_with_proxy(self):
"""Test that proxy URL is passed to client."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = '{"data": "test"}'
mock_response.headers = {"content-type": "application/json"}
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client_class.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_class.return_value.__aexit__ = AsyncMock(return_value=None)
await fetch_url(
"https://example.com/data",
DEFAULT_USER_AGENT_AUTONOMOUS,
proxy_url="http://proxy.example.com:8080"
)
# Verify AsyncClient was called with proxy
mock_client_class.assert_called_once_with(proxies="http://proxy.example.com:8080")

1444
src/fetch/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs/promises';
import * as os from 'os';
const SERVER_PATH = path.join(__dirname, '..', 'dist', 'index.js');
/**
* Spawns the filesystem server with given arguments and returns exit info
*/
async function spawnServer(args: string[], timeoutMs = 2000): Promise<{ exitCode: number | null; stderr: string }> {
return new Promise((resolve) => {
const proc = spawn('node', [SERVER_PATH, ...args], {
stdio: ['pipe', 'pipe', 'pipe'],
});
let stderr = '';
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
const timeout = setTimeout(() => {
proc.kill('SIGTERM');
}, timeoutMs);
proc.on('close', (code) => {
clearTimeout(timeout);
resolve({ exitCode: code, stderr });
});
proc.on('error', (err) => {
clearTimeout(timeout);
resolve({ exitCode: 1, stderr: err.message });
});
});
}
describe('Startup Directory Validation', () => {
let testDir: string;
let accessibleDir: string;
let accessibleDir2: string;
beforeEach(async () => {
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-startup-test-'));
accessibleDir = path.join(testDir, 'accessible');
accessibleDir2 = path.join(testDir, 'accessible2');
await fs.mkdir(accessibleDir, { recursive: true });
await fs.mkdir(accessibleDir2, { recursive: true });
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('should start successfully with all accessible directories', async () => {
const result = await spawnServer([accessibleDir, accessibleDir2]);
// Server starts and runs (we kill it after timeout, so exit code is null or from SIGTERM)
expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
expect(result.stderr).not.toContain('Error:');
});
it('should skip inaccessible directory and continue with accessible one', async () => {
const nonExistentDir = path.join(testDir, 'non-existent-dir-12345');
const result = await spawnServer([nonExistentDir, accessibleDir]);
// Should warn about inaccessible directory
expect(result.stderr).toContain('Warning: Cannot access directory');
expect(result.stderr).toContain(nonExistentDir);
// Should still start successfully
expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
});
it('should exit with error when ALL directories are inaccessible', async () => {
const nonExistent1 = path.join(testDir, 'non-existent-1');
const nonExistent2 = path.join(testDir, 'non-existent-2');
const result = await spawnServer([nonExistent1, nonExistent2]);
// Should exit with error
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('Error: None of the specified directories are accessible');
});
it('should warn when path is not a directory', async () => {
const filePath = path.join(testDir, 'not-a-directory.txt');
await fs.writeFile(filePath, 'content');
const result = await spawnServer([filePath, accessibleDir]);
// Should warn about non-directory
expect(result.stderr).toContain('Warning:');
expect(result.stderr).toContain('not a directory');
// Should still start with the valid directory
expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
});
});

View File

@@ -56,19 +56,28 @@ let allowedDirectories = await Promise.all(
})
);
// Validate that all directories exist and are accessible
await Promise.all(allowedDirectories.map(async (dir) => {
// Filter to only accessible directories, warn about inaccessible ones
const accessibleDirectories: string[] = [];
for (const dir of allowedDirectories) {
try {
const stats = await fs.stat(dir);
if (!stats.isDirectory()) {
console.error(`Error: ${dir} is not a directory`);
process.exit(1);
if (stats.isDirectory()) {
accessibleDirectories.push(dir);
} else {
console.error(`Warning: ${dir} is not a directory, skipping`);
}
} catch (error) {
console.error(`Error accessing directory ${dir}:`, error);
process.exit(1);
console.error(`Warning: Cannot access directory ${dir}, skipping`);
}
}));
}
// Exit only if ALL paths are inaccessible (and some were specified)
if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) {
console.error("Error: None of the specified directories are accessible");
process.exit(1);
}
allowedDirectories = accessibleDirectories;
// Initialize the global allowedDirectories in lib.ts
setAllowedDirectories(allowedDirectories);

View File

@@ -2,9 +2,9 @@
"name": "@modelcontextprotocol/server-filesystem",
"version": "0.6.3",
"description": "MCP server for filesystem access",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-filesystem",
"author": "Anthropic, PBC (https://anthropic.com)",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"repository": {
@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"diff": "^8.0.3",
"glob": "^10.5.0",
"minimatch": "^10.0.1",

View File

@@ -2,9 +2,9 @@
"name": "@modelcontextprotocol/server-memory",
"version": "0.6.3",
"description": "MCP server for enabling memory for Claude through a knowledge graph",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-memory",
"author": "Anthropic, PBC (https://anthropic.com)",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"repository": {
@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
"@modelcontextprotocol/sdk": "^1.26.0"
},
"devDependencies": {
"@types/node": "^22",

View File

@@ -2,9 +2,9 @@
"name": "@modelcontextprotocol/server-sequential-thinking",
"version": "0.6.2",
"description": "MCP server for sequential thinking and problem solving",
"license": "MIT",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-sequential-thinking",
"author": "Anthropic, PBC (https://anthropic.com)",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"repository": {
@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.26.0",
"chalk": "^5.3.0",
"yargs": "^17.7.2"
},