Merge branch 'main' into add-mcp-docs-server-to-claude
This commit is contained in:
commit
6247e676fe
23 changed files with 4410 additions and 2499 deletions
3
.github/workflows/cli_tests.yml
vendored
3
.github/workflows/cli_tests.yml
vendored
|
|
@ -31,9 +31,6 @@ jobs:
|
||||||
- name: Build CLI
|
- name: Build CLI
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Explicitly pre-install test dependencies
|
|
||||||
run: npx -y @modelcontextprotocol/server-everything --help || true
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test
|
run: npm test
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
197
LICENSE
197
LICENSE
|
|
@ -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
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
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.
|
||||||
|
|
|
||||||
44
cli/__tests__/README.md
Normal file
44
cli/__tests__/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# CLI Tests
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run in watch mode (useful for test file changes; won't work on CLI source changes without rebuild)
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
npm run test:cli # cli.test.ts
|
||||||
|
npm run test:cli-tools # tools.test.ts
|
||||||
|
npm run test:cli-headers # headers.test.ts
|
||||||
|
npm run test:cli-metadata # metadata.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Files
|
||||||
|
|
||||||
|
- `cli.test.ts` - Basic CLI functionality: CLI mode, environment variables, config files, resources, prompts, logging, transport types
|
||||||
|
- `tools.test.ts` - Tool-related tests: Tool discovery, JSON argument parsing, error handling, prompts
|
||||||
|
- `headers.test.ts` - Header parsing and validation
|
||||||
|
- `metadata.test.ts` - Metadata functionality: General metadata, tool-specific metadata, parsing, merging, validation
|
||||||
|
|
||||||
|
## Helpers
|
||||||
|
|
||||||
|
The `helpers/` directory contains shared utilities:
|
||||||
|
|
||||||
|
- `cli-runner.ts` - Spawns CLI as subprocess and captures output
|
||||||
|
- `test-mcp-server.ts` - Standalone stdio MCP server script for stdio transport testing
|
||||||
|
- `instrumented-server.ts` - In-process MCP test server for HTTP/SSE transports with request recording
|
||||||
|
- `assertions.ts` - Custom assertion helpers for CLI output validation
|
||||||
|
- `fixtures.ts` - Test config file generators and temporary directory management
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tests run in parallel across files (Vitest default)
|
||||||
|
- Tests within a file run sequentially (we have isolated config files and ports, so we could get more aggressive if desired)
|
||||||
|
- Config files use `crypto.randomUUID()` for uniqueness in parallel execution
|
||||||
|
- HTTP/SSE servers use dynamic port allocation to avoid conflicts
|
||||||
|
- Coverage is not used because much of the code that we want to measure is run by a spawned process, so it can't be tracked by Vitest
|
||||||
|
- /sample-config.json is no longer used by tests - not clear if this file serves some other purpose so leaving it for now
|
||||||
|
- All tests now use built-in MCP test servers, there are no external dependencies on servers from a registry
|
||||||
871
cli/__tests__/cli.test.ts
Normal file
871
cli/__tests__/cli.test.ts
Normal file
|
|
@ -0,0 +1,871 @@
|
||||||
|
import { describe, it, beforeAll, afterAll, expect } from "vitest";
|
||||||
|
import { runCli } from "./helpers/cli-runner.js";
|
||||||
|
import {
|
||||||
|
expectCliSuccess,
|
||||||
|
expectCliFailure,
|
||||||
|
expectValidJson,
|
||||||
|
} from "./helpers/assertions.js";
|
||||||
|
import {
|
||||||
|
NO_SERVER_SENTINEL,
|
||||||
|
createSampleTestConfig,
|
||||||
|
createTestConfig,
|
||||||
|
createInvalidConfig,
|
||||||
|
deleteConfigFile,
|
||||||
|
} from "./helpers/fixtures.js";
|
||||||
|
import { getTestMcpServerCommand } from "./helpers/test-server-stdio.js";
|
||||||
|
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||||
|
import {
|
||||||
|
createEchoTool,
|
||||||
|
createTestServerInfo,
|
||||||
|
} from "./helpers/test-fixtures.js";
|
||||||
|
|
||||||
|
describe("CLI Tests", () => {
|
||||||
|
describe("Basic CLI Mode", () => {
|
||||||
|
it("should execute tools/list successfully", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
|
||||||
|
// Validate expected tools from test-mcp-server
|
||||||
|
const toolNames = json.tools.map((tool: any) => tool.name);
|
||||||
|
expect(toolNames).toContain("echo");
|
||||||
|
expect(toolNames).toContain("get-sum");
|
||||||
|
expect(toolNames).toContain("get-annotated-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with nonexistent method", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
NO_SERVER_SENTINEL,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"nonexistent/method",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail without method", async () => {
|
||||||
|
const result = await runCli([NO_SERVER_SENTINEL, "--cli"]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Environment Variables", () => {
|
||||||
|
it("should accept environment variables", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"-e",
|
||||||
|
"KEY1=value1",
|
||||||
|
"-e",
|
||||||
|
"KEY2=value2",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("contents");
|
||||||
|
expect(Array.isArray(json.contents)).toBe(true);
|
||||||
|
expect(json.contents.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Parse the env vars from the resource
|
||||||
|
const envVars = JSON.parse(json.contents[0].text);
|
||||||
|
expect(envVars.KEY1).toBe("value1");
|
||||||
|
expect(envVars.KEY2).toBe("value2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invalid environment variable format", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
NO_SERVER_SENTINEL,
|
||||||
|
"-e",
|
||||||
|
"INVALID_FORMAT",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle environment variable with equals sign in value", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"-e",
|
||||||
|
"API_KEY=abc123=xyz789==",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
const envVars = JSON.parse(json.contents[0].text);
|
||||||
|
expect(envVars.API_KEY).toBe("abc123=xyz789==");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle environment variable with base64-encoded value", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"-e",
|
||||||
|
"JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
const envVars = JSON.parse(json.contents[0].text);
|
||||||
|
expect(envVars.JWT_TOKEN).toBe(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Config File", () => {
|
||||||
|
it("should use config file with CLI mode", async () => {
|
||||||
|
const configPath = createSampleTestConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when using config file without server name", async () => {
|
||||||
|
const configPath = createSampleTestConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when using server name without config file", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with nonexistent config file", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
"./nonexistent-config.json",
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with invalid config file format", async () => {
|
||||||
|
// Create invalid config temporarily
|
||||||
|
const invalidConfigPath = createInvalidConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
invalidConfigPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(invalidConfigPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with nonexistent server in config", async () => {
|
||||||
|
const configPath = createSampleTestConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"nonexistent",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Resource Options", () => {
|
||||||
|
it("should read resource with URI", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"demo://resource/static/document/architecture.md",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("contents");
|
||||||
|
expect(Array.isArray(json.contents)).toBe(true);
|
||||||
|
expect(json.contents.length).toBeGreaterThan(0);
|
||||||
|
expect(json.contents[0]).toHaveProperty(
|
||||||
|
"uri",
|
||||||
|
"demo://resource/static/document/architecture.md",
|
||||||
|
);
|
||||||
|
expect(json.contents[0]).toHaveProperty("mimeType", "text/markdown");
|
||||||
|
expect(json.contents[0]).toHaveProperty("text");
|
||||||
|
expect(json.contents[0].text).toContain("Architecture Documentation");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when reading resource without URI", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Prompt Options", () => {
|
||||||
|
it("should get prompt by name", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"simple-prompt",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("messages");
|
||||||
|
expect(Array.isArray(json.messages)).toBe(true);
|
||||||
|
expect(json.messages.length).toBeGreaterThan(0);
|
||||||
|
expect(json.messages[0]).toHaveProperty("role", "user");
|
||||||
|
expect(json.messages[0]).toHaveProperty("content");
|
||||||
|
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||||
|
expect(json.messages[0].content.text).toBe(
|
||||||
|
"This is a simple prompt for testing purposes.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get prompt with arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"args-prompt",
|
||||||
|
"--prompt-args",
|
||||||
|
"city=New York",
|
||||||
|
"state=NY",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("messages");
|
||||||
|
expect(Array.isArray(json.messages)).toBe(true);
|
||||||
|
expect(json.messages.length).toBeGreaterThan(0);
|
||||||
|
expect(json.messages[0]).toHaveProperty("role", "user");
|
||||||
|
expect(json.messages[0]).toHaveProperty("content");
|
||||||
|
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||||
|
// Verify that the arguments were actually used in the response
|
||||||
|
expect(json.messages[0].content.text).toContain("city=New York");
|
||||||
|
expect(json.messages[0].content.text).toContain("state=NY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when getting prompt without name", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Logging Options", () => {
|
||||||
|
it("should set log level", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
logging: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"logging/setLevel",
|
||||||
|
"--log-level",
|
||||||
|
"debug",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
// Validate the response - logging/setLevel should return an empty result
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toEqual({});
|
||||||
|
|
||||||
|
// Validate that the server actually received and recorded the log level
|
||||||
|
expect(server.getCurrentLogLevel()).toBe("debug");
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invalid log level", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"logging/setLevel",
|
||||||
|
"--log-level",
|
||||||
|
"invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Combined Options", () => {
|
||||||
|
it("should handle config file with environment variables", async () => {
|
||||||
|
const configPath = createSampleTestConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"-e",
|
||||||
|
"CLI_ENV_VAR=cli_value",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("contents");
|
||||||
|
expect(Array.isArray(json.contents)).toBe(true);
|
||||||
|
expect(json.contents.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Parse the env vars from the resource
|
||||||
|
const envVars = JSON.parse(json.contents[0].text);
|
||||||
|
expect(envVars).toHaveProperty("CLI_ENV_VAR");
|
||||||
|
expect(envVars.CLI_ENV_VAR).toBe("cli_value");
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle all options together", async () => {
|
||||||
|
const configPath = createSampleTestConfig();
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"-e",
|
||||||
|
"CLI_ENV_VAR=cli_value",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=Hello",
|
||||||
|
"--log-level",
|
||||||
|
"debug",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
expect(json.content[0].text).toBe("Echo: Hello");
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Config Transport Types", () => {
|
||||||
|
it("should work with stdio transport type", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"test-stdio": {
|
||||||
|
type: "stdio",
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
env: {
|
||||||
|
TEST_ENV: "test-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// First validate tools/list works
|
||||||
|
const toolsResult = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(toolsResult);
|
||||||
|
const toolsJson = expectValidJson(toolsResult);
|
||||||
|
expect(toolsJson).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(toolsJson.tools)).toBe(true);
|
||||||
|
expect(toolsJson.tools.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Then validate env vars from config are passed to server
|
||||||
|
const envResult = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-stdio",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(envResult);
|
||||||
|
const envJson = expectValidJson(envResult);
|
||||||
|
const envVars = JSON.parse(envJson.contents[0].text);
|
||||||
|
expect(envVars).toHaveProperty("TEST_ENV");
|
||||||
|
expect(envVars.TEST_ENV).toBe("test-value");
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with SSE transport type in CLI mode (connection error)", async () => {
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"test-sse": {
|
||||||
|
type: "sse",
|
||||||
|
url: "http://localhost:3000/sse",
|
||||||
|
note: "Test SSE server",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-sse",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with HTTP transport type in CLI mode (connection error)", async () => {
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"test-http": {
|
||||||
|
type: "streamable-http",
|
||||||
|
url: "http://localhost:3001/mcp",
|
||||||
|
note: "Test HTTP server",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-http",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with legacy config without type field", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"test-legacy": {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
env: {
|
||||||
|
LEGACY_ENV: "legacy-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// First validate tools/list works
|
||||||
|
const toolsResult = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-legacy",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(toolsResult);
|
||||||
|
const toolsJson = expectValidJson(toolsResult);
|
||||||
|
expect(toolsJson).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(toolsJson.tools)).toBe(true);
|
||||||
|
expect(toolsJson.tools.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Then validate env vars from config are passed to server
|
||||||
|
const envResult = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--server",
|
||||||
|
"test-legacy",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://env",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(envResult);
|
||||||
|
const envJson = expectValidJson(envResult);
|
||||||
|
const envVars = JSON.parse(envJson.contents[0].text);
|
||||||
|
expect(envVars).toHaveProperty("LEGACY_ENV");
|
||||||
|
expect(envVars.LEGACY_ENV).toBe("legacy-value");
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Default Server Selection", () => {
|
||||||
|
it("should auto-select single server", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"only-server": {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require explicit server selection even with default-server key (multiple servers)", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"default-server": {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
},
|
||||||
|
"other-server": {
|
||||||
|
command: "node",
|
||||||
|
args: ["other.js"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require explicit server selection with multiple servers", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const configPath = createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
server1: {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
},
|
||||||
|
server2: {
|
||||||
|
command: "node",
|
||||||
|
args: ["other.js"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
deleteConfigFile(configPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTTP Transport", () => {
|
||||||
|
it("should infer HTTP transport from URL ending with /mcp", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with explicit --transport http flag", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with explicit transport flag and URL suffix", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when SSE transport is given to HTTP server", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--transport",
|
||||||
|
"sse",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when HTTP transport is specified without URL", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when SSE transport is specified without URL", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"--transport",
|
||||||
|
"sse",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
210
cli/__tests__/headers.test.ts
Normal file
210
cli/__tests__/headers.test.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { runCli } from "./helpers/cli-runner.js";
|
||||||
|
import {
|
||||||
|
expectCliFailure,
|
||||||
|
expectOutputContains,
|
||||||
|
expectCliSuccess,
|
||||||
|
} from "./helpers/assertions.js";
|
||||||
|
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||||
|
import {
|
||||||
|
createEchoTool,
|
||||||
|
createTestServerInfo,
|
||||||
|
} from "./helpers/test-fixtures.js";
|
||||||
|
|
||||||
|
describe("Header Parsing and Validation", () => {
|
||||||
|
describe("Valid Headers", () => {
|
||||||
|
it("should parse valid single header and send it to server", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
"Authorization: Bearer token123",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Check that the server received the request with the correct headers
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
expect(recordedRequests.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Find the tools/list request (should be the last one)
|
||||||
|
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest.method).toBe("tools/list");
|
||||||
|
|
||||||
|
// Express normalizes headers to lowercase
|
||||||
|
expect(toolsListRequest.headers).toHaveProperty("authorization");
|
||||||
|
expect(toolsListRequest.headers?.authorization).toBe("Bearer token123");
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiple headers", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
"Authorization: Bearer token123",
|
||||||
|
"--header",
|
||||||
|
"X-API-Key: secret123",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||||
|
expect(toolsListRequest.method).toBe("tools/list");
|
||||||
|
expect(toolsListRequest.headers?.authorization).toBe("Bearer token123");
|
||||||
|
expect(toolsListRequest.headers?.["x-api-key"]).toBe("secret123");
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle header with colons in value", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
"X-Time: 2023:12:25:10:30:45",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||||
|
expect(toolsListRequest.method).toBe("tools/list");
|
||||||
|
expect(toolsListRequest.headers?.["x-time"]).toBe(
|
||||||
|
"2023:12:25:10:30:45",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle whitespace in headers", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
" X-Header : value with spaces ",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||||
|
expect(toolsListRequest.method).toBe("tools/list");
|
||||||
|
// Header values should be trimmed by the CLI parser
|
||||||
|
expect(toolsListRequest.headers?.["x-header"]).toBe(
|
||||||
|
"value with spaces",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Invalid Header Formats", () => {
|
||||||
|
it("should reject header format without colon", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"https://example.com",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
"InvalidHeader",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
expectOutputContains(result, "Invalid header format");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject header format with empty name", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"https://example.com",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
": value",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
expectOutputContains(result, "Invalid header format");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject header format with empty value", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
"https://example.com",
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
"--header",
|
||||||
|
"Header:",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
expectOutputContains(result, "Invalid header format");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
52
cli/__tests__/helpers/assertions.ts
Normal file
52
cli/__tests__/helpers/assertions.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { expect } from "vitest";
|
||||||
|
import type { CliResult } from "./cli-runner.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that CLI command succeeded (exit code 0)
|
||||||
|
*/
|
||||||
|
export function expectCliSuccess(result: CliResult) {
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that CLI command failed (non-zero exit code)
|
||||||
|
*/
|
||||||
|
export function expectCliFailure(result: CliResult) {
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that output contains expected text
|
||||||
|
*/
|
||||||
|
export function expectOutputContains(result: CliResult, text: string) {
|
||||||
|
expect(result.output).toContain(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that output contains valid JSON
|
||||||
|
* Uses stdout (not stderr) since JSON is written to stdout and warnings go to stderr
|
||||||
|
*/
|
||||||
|
export function expectValidJson(result: CliResult) {
|
||||||
|
expect(() => JSON.parse(result.stdout)).not.toThrow();
|
||||||
|
return JSON.parse(result.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that output contains JSON with error flag
|
||||||
|
*/
|
||||||
|
export function expectJsonError(result: CliResult) {
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json.isError).toBe(true);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that output contains expected JSON structure
|
||||||
|
*/
|
||||||
|
export function expectJsonStructure(result: CliResult, expectedKeys: string[]) {
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expectedKeys.forEach((key) => {
|
||||||
|
expect(json).toHaveProperty(key);
|
||||||
|
});
|
||||||
|
return json;
|
||||||
|
}
|
||||||
98
cli/__tests__/helpers/cli-runner.ts
Normal file
98
cli/__tests__/helpers/cli-runner.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname } from "path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const CLI_PATH = resolve(__dirname, "../../build/cli.js");
|
||||||
|
|
||||||
|
export interface CliResult {
|
||||||
|
exitCode: number | null;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
output: string; // Combined stdout + stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CliOptions {
|
||||||
|
timeout?: number;
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the CLI with given arguments and capture output
|
||||||
|
*/
|
||||||
|
export async function runCli(
|
||||||
|
args: string[],
|
||||||
|
options: CliOptions = {},
|
||||||
|
): Promise<CliResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("node", [CLI_PATH, ...args], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: { ...process.env, ...options.env },
|
||||||
|
signal: options.signal,
|
||||||
|
// Kill child process tree on exit
|
||||||
|
detached: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
// Default timeout of 10 seconds (less than vitest's 15s)
|
||||||
|
const timeoutMs = options.timeout ?? 10000;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
// Kill the process and all its children
|
||||||
|
try {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
} else {
|
||||||
|
// On Unix, kill the process group
|
||||||
|
process.kill(-child.pid!, "SIGTERM");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Process might already be dead, try direct kill
|
||||||
|
try {
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
} catch (e2) {
|
||||||
|
// Process is definitely dead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject(new Error(`CLI command timed out after ${timeoutMs}ms`));
|
||||||
|
}
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve({
|
||||||
|
exitCode: code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
output: stdout + stderr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
89
cli/__tests__/helpers/fixtures.ts
Normal file
89
cli/__tests__/helpers/fixtures.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { getTestMcpServerCommand } from "./test-server-stdio.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel value for tests that don't need a real server
|
||||||
|
* (tests that expect failure before connecting)
|
||||||
|
*/
|
||||||
|
export const NO_SERVER_SENTINEL = "invalid-command-that-does-not-exist";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sample test config with test-stdio and test-http servers
|
||||||
|
* Returns a temporary config file path that should be cleaned up with deleteConfigFile()
|
||||||
|
* @param httpUrl - Optional full URL (including /mcp path) for test-http server.
|
||||||
|
* If not provided, uses a placeholder URL. The test-http server exists
|
||||||
|
* to test server selection logic and may not actually be used.
|
||||||
|
*/
|
||||||
|
export function createSampleTestConfig(httpUrl?: string): string {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
return createTestConfig({
|
||||||
|
mcpServers: {
|
||||||
|
"test-stdio": {
|
||||||
|
type: "stdio",
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
env: {
|
||||||
|
HELLO: "Hello MCP!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"test-http": {
|
||||||
|
type: "streamable-http",
|
||||||
|
url: httpUrl || "http://localhost:3001/mcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a temporary directory for test files
|
||||||
|
* Uses crypto.randomUUID() to ensure uniqueness even when called in parallel
|
||||||
|
*/
|
||||||
|
function createTempDir(prefix: string = "mcp-inspector-test-"): string {
|
||||||
|
const uniqueId = crypto.randomUUID();
|
||||||
|
const tempDir = path.join(os.tmpdir(), `${prefix}${uniqueId}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up temporary directory
|
||||||
|
*/
|
||||||
|
function cleanupTempDir(dir: string) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test config file
|
||||||
|
*/
|
||||||
|
export function createTestConfig(config: {
|
||||||
|
mcpServers: Record<string, any>;
|
||||||
|
}): string {
|
||||||
|
const tempDir = createTempDir("mcp-inspector-config-");
|
||||||
|
const configPath = path.join(tempDir, "config.json");
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an invalid config file (malformed JSON)
|
||||||
|
*/
|
||||||
|
export function createInvalidConfig(): string {
|
||||||
|
const tempDir = createTempDir("mcp-inspector-config-");
|
||||||
|
const configPath = path.join(tempDir, "invalid-config.json");
|
||||||
|
fs.writeFileSync(configPath, '{\n "mcpServers": {\n "invalid": {');
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a config file and its containing directory
|
||||||
|
*/
|
||||||
|
export function deleteConfigFile(configPath: string): void {
|
||||||
|
cleanupTempDir(path.dirname(configPath));
|
||||||
|
}
|
||||||
267
cli/__tests__/helpers/test-fixtures.ts
Normal file
267
cli/__tests__/helpers/test-fixtures.ts
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
/**
|
||||||
|
* Shared types and test fixtures for composable MCP test servers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
import { ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
||||||
|
|
||||||
|
type ToolInputSchema = ZodRawShapeCompat;
|
||||||
|
|
||||||
|
export interface ToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema?: ToolInputSchema;
|
||||||
|
handler: (params: Record<string, any>) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceDefinition {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PromptArgsSchema = ZodRawShapeCompat;
|
||||||
|
|
||||||
|
export interface PromptDefinition {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
argsSchema?: PromptArgsSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This allows us to compose tests servers using the metadata and features we want in a given scenario
|
||||||
|
export interface ServerConfig {
|
||||||
|
serverInfo: Implementation; // Server metadata (name, version, etc.) - required
|
||||||
|
tools?: ToolDefinition[]; // Tools to register (optional, empty array means no tools, but tools capability is still advertised)
|
||||||
|
resources?: ResourceDefinition[]; // Resources to register (optional, empty array means no resources, but resources capability is still advertised)
|
||||||
|
prompts?: PromptDefinition[]; // Prompts to register (optional, empty array means no prompts, but prompts capability is still advertised)
|
||||||
|
logging?: boolean; // Whether to advertise logging capability (default: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an "echo" tool that echoes back the input message
|
||||||
|
*/
|
||||||
|
export function createEchoTool(): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: "echo",
|
||||||
|
description: "Echo back the input message",
|
||||||
|
inputSchema: {
|
||||||
|
message: z.string().describe("Message to echo back"),
|
||||||
|
},
|
||||||
|
handler: async (params: Record<string, any>) => {
|
||||||
|
return { message: `Echo: ${params.message as string}` };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an "add" tool that adds two numbers together
|
||||||
|
*/
|
||||||
|
export function createAddTool(): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: "add",
|
||||||
|
description: "Add two numbers together",
|
||||||
|
inputSchema: {
|
||||||
|
a: z.number().describe("First number"),
|
||||||
|
b: z.number().describe("Second number"),
|
||||||
|
},
|
||||||
|
handler: async (params: Record<string, any>) => {
|
||||||
|
const a = params.a as number;
|
||||||
|
const b = params.b as number;
|
||||||
|
return { result: a + b };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "get-sum" tool that returns the sum of two numbers (alias for add)
|
||||||
|
*/
|
||||||
|
export function createGetSumTool(): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: "get-sum",
|
||||||
|
description: "Get the sum of two numbers",
|
||||||
|
inputSchema: {
|
||||||
|
a: z.number().describe("First number"),
|
||||||
|
b: z.number().describe("Second number"),
|
||||||
|
},
|
||||||
|
handler: async (params: Record<string, any>) => {
|
||||||
|
const a = params.a as number;
|
||||||
|
const b = params.b as number;
|
||||||
|
return { result: a + b };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "get-annotated-message" tool that returns a message with optional image
|
||||||
|
*/
|
||||||
|
export function createGetAnnotatedMessageTool(): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: "get-annotated-message",
|
||||||
|
description: "Get an annotated message",
|
||||||
|
inputSchema: {
|
||||||
|
messageType: z
|
||||||
|
.enum(["success", "error", "warning", "info"])
|
||||||
|
.describe("Type of message"),
|
||||||
|
includeImage: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Whether to include an image"),
|
||||||
|
},
|
||||||
|
handler: async (params: Record<string, any>) => {
|
||||||
|
const messageType = params.messageType as string;
|
||||||
|
const includeImage = params.includeImage as boolean | undefined;
|
||||||
|
const message = `This is a ${messageType} message`;
|
||||||
|
const content: Array<
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
| { type: "image"; data: string; mimeType: string }
|
||||||
|
> = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeImage) {
|
||||||
|
content.push({
|
||||||
|
type: "image",
|
||||||
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", // 1x1 transparent PNG
|
||||||
|
mimeType: "image/png",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "simple-prompt" prompt definition
|
||||||
|
*/
|
||||||
|
export function createSimplePrompt(): PromptDefinition {
|
||||||
|
return {
|
||||||
|
name: "simple-prompt",
|
||||||
|
description: "A simple prompt for testing",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an "args-prompt" prompt that accepts arguments
|
||||||
|
*/
|
||||||
|
export function createArgsPrompt(): PromptDefinition {
|
||||||
|
return {
|
||||||
|
name: "args-prompt",
|
||||||
|
description: "A prompt that accepts arguments for testing",
|
||||||
|
argsSchema: {
|
||||||
|
city: z.string().describe("City name"),
|
||||||
|
state: z.string().describe("State name"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an "architecture" resource definition
|
||||||
|
*/
|
||||||
|
export function createArchitectureResource(): ResourceDefinition {
|
||||||
|
return {
|
||||||
|
name: "architecture",
|
||||||
|
uri: "demo://resource/static/document/architecture.md",
|
||||||
|
description: "Architecture documentation",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
text: `# Architecture Documentation
|
||||||
|
|
||||||
|
This is a test resource for the MCP test server.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This resource is used for testing resource reading functionality in the CLI.
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
- Introduction
|
||||||
|
- Design
|
||||||
|
- Implementation
|
||||||
|
- Testing
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
This is a static resource provided by the test MCP server.
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "test-cwd" resource that exposes the current working directory (generally useful when testing with the stdio test server)
|
||||||
|
*/
|
||||||
|
export function createTestCwdResource(): ResourceDefinition {
|
||||||
|
return {
|
||||||
|
name: "test-cwd",
|
||||||
|
uri: "test://cwd",
|
||||||
|
description: "Current working directory of the test server",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: process.cwd(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "test-env" resource that exposes environment variables (generally useful when testing with the stdio test server)
|
||||||
|
*/
|
||||||
|
export function createTestEnvResource(): ResourceDefinition {
|
||||||
|
return {
|
||||||
|
name: "test-env",
|
||||||
|
uri: "test://env",
|
||||||
|
description: "Environment variables available to the test server",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify(process.env, null, 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a "test-argv" resource that exposes command-line arguments (generally useful when testing with the stdio test server)
|
||||||
|
*/
|
||||||
|
export function createTestArgvResource(): ResourceDefinition {
|
||||||
|
return {
|
||||||
|
name: "test-argv",
|
||||||
|
uri: "test://argv",
|
||||||
|
description: "Command-line arguments the test server was started with",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify(process.argv, null, 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create minimal server info for test servers
|
||||||
|
*/
|
||||||
|
export function createTestServerInfo(
|
||||||
|
name: string = "test-server",
|
||||||
|
version: string = "1.0.0",
|
||||||
|
): Implementation {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default server config with common test tools, prompts, and resources
|
||||||
|
*/
|
||||||
|
export function getDefaultServerConfig(): ServerConfig {
|
||||||
|
return {
|
||||||
|
serverInfo: createTestServerInfo("test-mcp-server", "1.0.0"),
|
||||||
|
tools: [
|
||||||
|
createEchoTool(),
|
||||||
|
createGetSumTool(),
|
||||||
|
createGetAnnotatedMessageTool(),
|
||||||
|
],
|
||||||
|
prompts: [createSimplePrompt(), createArgsPrompt()],
|
||||||
|
resources: [
|
||||||
|
createArchitectureResource(),
|
||||||
|
createTestCwdResource(),
|
||||||
|
createTestEnvResource(),
|
||||||
|
createTestArgvResource(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
443
cli/__tests__/helpers/test-server-http.ts
Normal file
443
cli/__tests__/helpers/test-server-http.ts
Normal file
|
|
@ -0,0 +1,443 @@
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||||
|
import { SetLevelRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import express from "express";
|
||||||
|
import { createServer as createHttpServer, Server as HttpServer } from "http";
|
||||||
|
import { createServer as createNetServer } from "net";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
import type { ServerConfig } from "./test-fixtures.js";
|
||||||
|
|
||||||
|
export interface RecordedRequest {
|
||||||
|
method: string;
|
||||||
|
params?: any;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
response: any;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an available port starting from the given port
|
||||||
|
*/
|
||||||
|
async function findAvailablePort(startPort: number): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = createNetServer();
|
||||||
|
server.listen(startPort, () => {
|
||||||
|
const port = (server.address() as { port: number })?.port;
|
||||||
|
server.close(() => resolve(port || startPort));
|
||||||
|
});
|
||||||
|
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||||
|
if (err.code === "EADDRINUSE") {
|
||||||
|
// Try next port
|
||||||
|
findAvailablePort(startPort + 1)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract headers from Express request
|
||||||
|
*/
|
||||||
|
function extractHeaders(req: Request): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
headers[key] = value;
|
||||||
|
} else if (Array.isArray(value) && value.length > 0) {
|
||||||
|
headers[key] = value[value.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With this test server, your test can hold an instance and you can get the server's recorded message history at any time.
|
||||||
|
//
|
||||||
|
export class TestServerHttp {
|
||||||
|
private mcpServer: McpServer;
|
||||||
|
private config: ServerConfig;
|
||||||
|
private recordedRequests: RecordedRequest[] = [];
|
||||||
|
private httpServer?: HttpServer;
|
||||||
|
private transport?: StreamableHTTPServerTransport | SSEServerTransport;
|
||||||
|
private url?: string;
|
||||||
|
private currentRequestHeaders?: Record<string, string>;
|
||||||
|
private currentLogLevel: string | null = null;
|
||||||
|
|
||||||
|
constructor(config: ServerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
const capabilities: {
|
||||||
|
tools?: {};
|
||||||
|
resources?: {};
|
||||||
|
prompts?: {};
|
||||||
|
logging?: {};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Only include capabilities for features that are present in config
|
||||||
|
if (config.tools !== undefined) {
|
||||||
|
capabilities.tools = {};
|
||||||
|
}
|
||||||
|
if (config.resources !== undefined) {
|
||||||
|
capabilities.resources = {};
|
||||||
|
}
|
||||||
|
if (config.prompts !== undefined) {
|
||||||
|
capabilities.prompts = {};
|
||||||
|
}
|
||||||
|
if (config.logging === true) {
|
||||||
|
capabilities.logging = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mcpServer = new McpServer(config.serverInfo, {
|
||||||
|
capabilities,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
if (config.logging === true) {
|
||||||
|
this.setupLoggingHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
// Set up tools
|
||||||
|
if (this.config.tools && this.config.tools.length > 0) {
|
||||||
|
for (const tool of this.config.tools) {
|
||||||
|
this.mcpServer.registerTool(
|
||||||
|
tool.name,
|
||||||
|
{
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const result = await tool.handler(args as Record<string, any>);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up resources
|
||||||
|
if (this.config.resources && this.config.resources.length > 0) {
|
||||||
|
for (const resource of this.config.resources) {
|
||||||
|
this.mcpServer.registerResource(
|
||||||
|
resource.name,
|
||||||
|
resource.uri,
|
||||||
|
{
|
||||||
|
description: resource.description,
|
||||||
|
mimeType: resource.mimeType,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: resource.uri,
|
||||||
|
mimeType: resource.mimeType || "text/plain",
|
||||||
|
text: resource.text || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up prompts
|
||||||
|
if (this.config.prompts && this.config.prompts.length > 0) {
|
||||||
|
for (const prompt of this.config.prompts) {
|
||||||
|
this.mcpServer.registerPrompt(
|
||||||
|
prompt.name,
|
||||||
|
{
|
||||||
|
description: prompt.description,
|
||||||
|
argsSchema: prompt.argsSchema,
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
// Return a simple prompt response
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `Prompt: ${prompt.name}${args ? ` with args: ${JSON.stringify(args)}` : ""}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupLoggingHandler() {
|
||||||
|
// Intercept logging/setLevel requests to track the level
|
||||||
|
this.mcpServer.server.setRequestHandler(
|
||||||
|
SetLevelRequestSchema,
|
||||||
|
async (request) => {
|
||||||
|
this.currentLogLevel = request.params.level;
|
||||||
|
// Return empty result as per MCP spec
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server with the specified transport
|
||||||
|
*/
|
||||||
|
async start(
|
||||||
|
transport: "http" | "sse",
|
||||||
|
requestedPort?: number,
|
||||||
|
): Promise<number> {
|
||||||
|
const port = requestedPort
|
||||||
|
? await findAvailablePort(requestedPort)
|
||||||
|
: await findAvailablePort(transport === "http" ? 3001 : 3000);
|
||||||
|
|
||||||
|
this.url = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
if (transport === "http") {
|
||||||
|
return this.startHttp(port);
|
||||||
|
} else {
|
||||||
|
return this.startSse(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startHttp(port: number): Promise<number> {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
this.httpServer = createHttpServer(app);
|
||||||
|
|
||||||
|
// Create StreamableHTTP transport
|
||||||
|
this.transport = new StreamableHTTPServerTransport({});
|
||||||
|
|
||||||
|
// Set up Express route to handle MCP requests
|
||||||
|
app.post("/mcp", async (req: Request, res: Response) => {
|
||||||
|
// Capture headers for this request
|
||||||
|
this.currentRequestHeaders = extractHeaders(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (this.transport as StreamableHTTPServerTransport).handleRequest(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept messages to record them
|
||||||
|
const originalOnMessage = this.transport.onmessage;
|
||||||
|
this.transport.onmessage = async (message) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const method =
|
||||||
|
"method" in message && typeof message.method === "string"
|
||||||
|
? message.method
|
||||||
|
: "unknown";
|
||||||
|
const params = "params" in message ? message.params : undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract metadata from params if present
|
||||||
|
const metadata =
|
||||||
|
params && typeof params === "object" && "_meta" in params
|
||||||
|
? ((params as any)._meta as Record<string, string>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Let the server handle the message
|
||||||
|
if (originalOnMessage) {
|
||||||
|
await originalOnMessage.call(this.transport, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record successful request (response will be sent by transport)
|
||||||
|
// Note: We can't easily capture the response here, so we'll record
|
||||||
|
// that the request was processed
|
||||||
|
this.recordedRequests.push({
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
headers: { ...this.currentRequestHeaders },
|
||||||
|
metadata: metadata ? { ...metadata } : undefined,
|
||||||
|
response: { processed: true },
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Extract metadata from params if present
|
||||||
|
const metadata =
|
||||||
|
params && typeof params === "object" && "_meta" in params
|
||||||
|
? ((params as any)._meta as Record<string, string>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Record error
|
||||||
|
this.recordedRequests.push({
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
headers: { ...this.currentRequestHeaders },
|
||||||
|
metadata: metadata ? { ...metadata } : undefined,
|
||||||
|
response: {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect transport to server
|
||||||
|
await this.mcpServer.connect(this.transport);
|
||||||
|
|
||||||
|
// Start listening
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.httpServer!.listen(port, () => {
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
this.httpServer!.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startSse(port: number): Promise<number> {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
this.httpServer = createHttpServer(app);
|
||||||
|
|
||||||
|
// For SSE, we need to set up an Express route that creates the transport per request
|
||||||
|
// This is a simplified version - SSE transport is created per connection
|
||||||
|
app.get("/mcp", async (req: Request, res: Response) => {
|
||||||
|
this.currentRequestHeaders = extractHeaders(req);
|
||||||
|
const sseTransport = new SSEServerTransport("/mcp", res);
|
||||||
|
|
||||||
|
// Intercept messages
|
||||||
|
const originalOnMessage = sseTransport.onmessage;
|
||||||
|
sseTransport.onmessage = async (message) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const method =
|
||||||
|
"method" in message && typeof message.method === "string"
|
||||||
|
? message.method
|
||||||
|
: "unknown";
|
||||||
|
const params = "params" in message ? message.params : undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract metadata from params if present
|
||||||
|
const metadata =
|
||||||
|
params && typeof params === "object" && "_meta" in params
|
||||||
|
? ((params as any)._meta as Record<string, string>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (originalOnMessage) {
|
||||||
|
await originalOnMessage.call(sseTransport, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordedRequests.push({
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
headers: { ...this.currentRequestHeaders },
|
||||||
|
metadata: metadata ? { ...metadata } : undefined,
|
||||||
|
response: { processed: true },
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Extract metadata from params if present
|
||||||
|
const metadata =
|
||||||
|
params && typeof params === "object" && "_meta" in params
|
||||||
|
? ((params as any)._meta as Record<string, string>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
this.recordedRequests.push({
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
headers: { ...this.currentRequestHeaders },
|
||||||
|
metadata: metadata ? { ...metadata } : undefined,
|
||||||
|
response: {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.mcpServer.connect(sseTransport);
|
||||||
|
await sseTransport.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: SSE transport is created per request, so we don't store a single instance
|
||||||
|
this.transport = undefined;
|
||||||
|
|
||||||
|
// Start listening
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.httpServer!.listen(port, () => {
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
this.httpServer!.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the server
|
||||||
|
*/
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
await this.mcpServer.close();
|
||||||
|
|
||||||
|
if (this.transport) {
|
||||||
|
await this.transport.close();
|
||||||
|
this.transport = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.httpServer) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Force close all connections
|
||||||
|
this.httpServer!.closeAllConnections?.();
|
||||||
|
this.httpServer!.close(() => {
|
||||||
|
this.httpServer = undefined;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recorded requests
|
||||||
|
*/
|
||||||
|
getRecordedRequests(): RecordedRequest[] {
|
||||||
|
return [...this.recordedRequests];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear recorded requests
|
||||||
|
*/
|
||||||
|
clearRecordings(): void {
|
||||||
|
this.recordedRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server URL
|
||||||
|
*/
|
||||||
|
getUrl(): string {
|
||||||
|
if (!this.url) {
|
||||||
|
throw new Error("Server not started");
|
||||||
|
}
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the most recent log level that was set
|
||||||
|
*/
|
||||||
|
getCurrentLogLevel(): string | null {
|
||||||
|
return this.currentLogLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTP/SSE MCP test server
|
||||||
|
*/
|
||||||
|
export function createTestServerHttp(config: ServerConfig): TestServerHttp {
|
||||||
|
return new TestServerHttp(config);
|
||||||
|
}
|
||||||
241
cli/__tests__/helpers/test-server-stdio.ts
Normal file
241
cli/__tests__/helpers/test-server-stdio.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test MCP server for stdio transport testing
|
||||||
|
* Can be used programmatically or run as a standalone executable
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import type {
|
||||||
|
ServerConfig,
|
||||||
|
ToolDefinition,
|
||||||
|
PromptDefinition,
|
||||||
|
ResourceDefinition,
|
||||||
|
} from "./test-fixtures.js";
|
||||||
|
import { getDefaultServerConfig } from "./test-fixtures.js";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export class TestServerStdio {
|
||||||
|
private mcpServer: McpServer;
|
||||||
|
private config: ServerConfig;
|
||||||
|
private transport?: StdioServerTransport;
|
||||||
|
|
||||||
|
constructor(config: ServerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
const capabilities: {
|
||||||
|
tools?: {};
|
||||||
|
resources?: {};
|
||||||
|
prompts?: {};
|
||||||
|
logging?: {};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Only include capabilities for features that are present in config
|
||||||
|
if (config.tools !== undefined) {
|
||||||
|
capabilities.tools = {};
|
||||||
|
}
|
||||||
|
if (config.resources !== undefined) {
|
||||||
|
capabilities.resources = {};
|
||||||
|
}
|
||||||
|
if (config.prompts !== undefined) {
|
||||||
|
capabilities.prompts = {};
|
||||||
|
}
|
||||||
|
if (config.logging === true) {
|
||||||
|
capabilities.logging = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mcpServer = new McpServer(config.serverInfo, {
|
||||||
|
capabilities,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
// Set up tools
|
||||||
|
if (this.config.tools && this.config.tools.length > 0) {
|
||||||
|
for (const tool of this.config.tools) {
|
||||||
|
this.mcpServer.registerTool(
|
||||||
|
tool.name,
|
||||||
|
{
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const result = await tool.handler(args as Record<string, any>);
|
||||||
|
// If handler returns content array directly (like get-annotated-message), use it
|
||||||
|
if (result && Array.isArray(result.content)) {
|
||||||
|
return { content: result.content };
|
||||||
|
}
|
||||||
|
// If handler returns message (like echo), format it
|
||||||
|
if (result && typeof result.message === "string") {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: result.message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Otherwise, stringify the result
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up resources
|
||||||
|
if (this.config.resources && this.config.resources.length > 0) {
|
||||||
|
for (const resource of this.config.resources) {
|
||||||
|
this.mcpServer.registerResource(
|
||||||
|
resource.name,
|
||||||
|
resource.uri,
|
||||||
|
{
|
||||||
|
description: resource.description,
|
||||||
|
mimeType: resource.mimeType,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// For dynamic resources, get fresh text
|
||||||
|
let text = resource.text;
|
||||||
|
if (resource.name === "test-cwd") {
|
||||||
|
text = process.cwd();
|
||||||
|
} else if (resource.name === "test-env") {
|
||||||
|
text = JSON.stringify(process.env, null, 2);
|
||||||
|
} else if (resource.name === "test-argv") {
|
||||||
|
text = JSON.stringify(process.argv, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: resource.uri,
|
||||||
|
mimeType: resource.mimeType || "text/plain",
|
||||||
|
text: text || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up prompts
|
||||||
|
if (this.config.prompts && this.config.prompts.length > 0) {
|
||||||
|
for (const prompt of this.config.prompts) {
|
||||||
|
this.mcpServer.registerPrompt(
|
||||||
|
prompt.name,
|
||||||
|
{
|
||||||
|
description: prompt.description,
|
||||||
|
argsSchema: prompt.argsSchema,
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
if (prompt.name === "args-prompt" && args) {
|
||||||
|
const city = (args as any).city as string;
|
||||||
|
const state = (args as any).state as string;
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `This is a prompt with arguments: city=${city}, state=${state}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: "This is a simple prompt for testing purposes.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server with stdio transport
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
this.transport = new StdioServerTransport();
|
||||||
|
await this.mcpServer.connect(this.transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the server
|
||||||
|
*/
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
await this.mcpServer.close();
|
||||||
|
if (this.transport) {
|
||||||
|
await this.transport.close();
|
||||||
|
this.transport = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a stdio MCP test server
|
||||||
|
*/
|
||||||
|
export function createTestServerStdio(config: ServerConfig): TestServerStdio {
|
||||||
|
return new TestServerStdio(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the test MCP server script
|
||||||
|
*/
|
||||||
|
export function getTestMcpServerPath(): string {
|
||||||
|
return path.resolve(__dirname, "test-server-stdio.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the command and args to run the test MCP server
|
||||||
|
*/
|
||||||
|
export function getTestMcpServerCommand(): { command: string; args: string[] } {
|
||||||
|
return {
|
||||||
|
command: "tsx",
|
||||||
|
args: [getTestMcpServerPath()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If run as a standalone script, start with default config
|
||||||
|
// Check if this file is being executed directly (not imported)
|
||||||
|
const isMainModule =
|
||||||
|
import.meta.url.endsWith(process.argv[1]) ||
|
||||||
|
process.argv[1]?.endsWith("test-server-stdio.ts") ||
|
||||||
|
process.argv[1]?.endsWith("test-server-stdio.js");
|
||||||
|
|
||||||
|
if (isMainModule) {
|
||||||
|
const server = new TestServerStdio(getDefaultServerConfig());
|
||||||
|
server
|
||||||
|
.start()
|
||||||
|
.then(() => {
|
||||||
|
// Server is now running and listening on stdio
|
||||||
|
// Keep the process alive
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to start test MCP server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
933
cli/__tests__/metadata.test.ts
Normal file
933
cli/__tests__/metadata.test.ts
Normal file
|
|
@ -0,0 +1,933 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { runCli } from "./helpers/cli-runner.js";
|
||||||
|
import {
|
||||||
|
expectCliSuccess,
|
||||||
|
expectCliFailure,
|
||||||
|
expectValidJson,
|
||||||
|
} from "./helpers/assertions.js";
|
||||||
|
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||||
|
import {
|
||||||
|
createEchoTool,
|
||||||
|
createAddTool,
|
||||||
|
createTestServerInfo,
|
||||||
|
} from "./helpers/test-fixtures.js";
|
||||||
|
import { NO_SERVER_SENTINEL } from "./helpers/fixtures.js";
|
||||||
|
|
||||||
|
describe("Metadata Tests", () => {
|
||||||
|
describe("General Metadata", () => {
|
||||||
|
it("should work with tools/list", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({ client: "test-client" });
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with resources/list", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: "test://resource",
|
||||||
|
name: "test-resource",
|
||||||
|
text: "test content",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/list",
|
||||||
|
"--metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("resources");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const resourcesListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "resources/list",
|
||||||
|
);
|
||||||
|
expect(resourcesListRequest).toBeDefined();
|
||||||
|
expect(resourcesListRequest?.metadata).toEqual({
|
||||||
|
client: "test-client",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with prompts/list", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: "test-prompt",
|
||||||
|
description: "A test prompt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/list",
|
||||||
|
"--metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("prompts");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const promptsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "prompts/list",
|
||||||
|
);
|
||||||
|
expect(promptsListRequest).toBeDefined();
|
||||||
|
expect(promptsListRequest?.metadata).toEqual({
|
||||||
|
client: "test-client",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with resources/read", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: "test://resource",
|
||||||
|
name: "test-resource",
|
||||||
|
text: "test content",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/read",
|
||||||
|
"--uri",
|
||||||
|
"test://resource",
|
||||||
|
"--metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("contents");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const readRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "resources/read",
|
||||||
|
);
|
||||||
|
expect(readRequest).toBeDefined();
|
||||||
|
expect(readRequest?.metadata).toEqual({ client: "test-client" });
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with prompts/get", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: "test-prompt",
|
||||||
|
description: "A test prompt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"test-prompt",
|
||||||
|
"--metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("messages");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const getPromptRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "prompts/get",
|
||||||
|
);
|
||||||
|
expect(getPromptRequest).toBeDefined();
|
||||||
|
expect(getPromptRequest?.metadata).toEqual({ client: "test-client" });
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Tool-Specific Metadata", () => {
|
||||||
|
it("should work with tools/call", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=hello world",
|
||||||
|
"--tool-metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({ client: "test-client" });
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with complex tool", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createAddTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"add",
|
||||||
|
"--tool-arg",
|
||||||
|
"a=10",
|
||||||
|
"b=20",
|
||||||
|
"--tool-metadata",
|
||||||
|
"client=test-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({ client: "test-client" });
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Merging", () => {
|
||||||
|
it("should merge general and tool-specific metadata (tool-specific overrides)", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=hello world",
|
||||||
|
"--metadata",
|
||||||
|
"client=general-client",
|
||||||
|
"shared_key=shared_value",
|
||||||
|
"--tool-metadata",
|
||||||
|
"client=tool-specific-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate metadata was merged correctly (tool-specific overrides general)
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({
|
||||||
|
client: "tool-specific-client", // Tool-specific overrides general
|
||||||
|
shared_key: "shared_value", // General metadata is preserved
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Parsing", () => {
|
||||||
|
it("should handle numeric values", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"integer_value=42",
|
||||||
|
"decimal_value=3.14159",
|
||||||
|
"negative_value=-10",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate metadata values are sent as strings
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
integer_value: "42",
|
||||||
|
decimal_value: "3.14159",
|
||||||
|
negative_value: "-10",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle JSON values", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
'json_object="{\\"key\\":\\"value\\"}"',
|
||||||
|
'json_array="[1,2,3]"',
|
||||||
|
'json_string="\\"quoted\\""',
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate JSON values are sent as strings
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
json_object: '{"key":"value"}',
|
||||||
|
json_array: "[1,2,3]",
|
||||||
|
json_string: '"quoted"',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle special characters", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"unicode=🚀🎉✨",
|
||||||
|
"special_chars=!@#$%^&*()",
|
||||||
|
"spaces=hello world with spaces",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate special characters are preserved
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
unicode: "🚀🎉✨",
|
||||||
|
special_chars: "!@#$%^&*()",
|
||||||
|
spaces: "hello world with spaces",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Edge Cases", () => {
|
||||||
|
it("should handle single metadata entry", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"single_key=single_value",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate single metadata entry
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
single_key: "single_value",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle many metadata entries", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"key1=value1",
|
||||||
|
"key2=value2",
|
||||||
|
"key3=value3",
|
||||||
|
"key4=value4",
|
||||||
|
"key5=value5",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate all metadata entries
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
key1: "value1",
|
||||||
|
key2: "value2",
|
||||||
|
key3: "value3",
|
||||||
|
key4: "value4",
|
||||||
|
key5: "value5",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Error Cases", () => {
|
||||||
|
it("should fail with invalid metadata format (missing equals)", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
NO_SERVER_SENTINEL,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"invalid_format_no_equals",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with invalid tool-metadata format (missing equals)", async () => {
|
||||||
|
const result = await runCli([
|
||||||
|
NO_SERVER_SENTINEL,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=test",
|
||||||
|
"--tool-metadata",
|
||||||
|
"invalid_format_no_equals",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Impact", () => {
|
||||||
|
it("should handle tool-specific metadata precedence over general", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=precedence test",
|
||||||
|
"--metadata",
|
||||||
|
"client=general-client",
|
||||||
|
"--tool-metadata",
|
||||||
|
"client=tool-specific-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate tool-specific metadata overrides general
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({
|
||||||
|
client: "tool-specific-client",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with resources methods", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: "test://resource",
|
||||||
|
name: "test-resource",
|
||||||
|
text: "test content",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"resources/list",
|
||||||
|
"--metadata",
|
||||||
|
"resource_client=test-resource-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const resourcesListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "resources/list",
|
||||||
|
);
|
||||||
|
expect(resourcesListRequest).toBeDefined();
|
||||||
|
expect(resourcesListRequest?.metadata).toEqual({
|
||||||
|
resource_client: "test-resource-client",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with prompts methods", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: "test-prompt",
|
||||||
|
description: "A test prompt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"test-prompt",
|
||||||
|
"--metadata",
|
||||||
|
"prompt_client=test-prompt-client",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const getPromptRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "prompts/get",
|
||||||
|
);
|
||||||
|
expect(getPromptRequest).toBeDefined();
|
||||||
|
expect(getPromptRequest?.metadata).toEqual({
|
||||||
|
prompt_client: "test-prompt-client",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Validation", () => {
|
||||||
|
it("should handle special characters in keys", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=special keys test",
|
||||||
|
"--metadata",
|
||||||
|
"key-with-dashes=value1",
|
||||||
|
"key_with_underscores=value2",
|
||||||
|
"key.with.dots=value3",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate special characters in keys are preserved
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({
|
||||||
|
"key-with-dashes": "value1",
|
||||||
|
key_with_underscores: "value2",
|
||||||
|
"key.with.dots": "value3",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Metadata Integration", () => {
|
||||||
|
it("should work with all MCP methods", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
"--metadata",
|
||||||
|
"integration_test=true",
|
||||||
|
"test_phase=all_methods",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate metadata was sent
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolsListRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/list",
|
||||||
|
);
|
||||||
|
expect(toolsListRequest).toBeDefined();
|
||||||
|
expect(toolsListRequest?.metadata).toEqual({
|
||||||
|
integration_test: "true",
|
||||||
|
test_phase: "all_methods",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex metadata scenario", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=complex test",
|
||||||
|
"--metadata",
|
||||||
|
"session_id=12345",
|
||||||
|
"user_id=67890",
|
||||||
|
"timestamp=2024-01-01T00:00:00Z",
|
||||||
|
"request_id=req-abc-123",
|
||||||
|
"--tool-metadata",
|
||||||
|
"tool_session=session-xyz-789",
|
||||||
|
"execution_context=test",
|
||||||
|
"priority=high",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate complex metadata merging
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({
|
||||||
|
session_id: "12345",
|
||||||
|
user_id: "67890",
|
||||||
|
timestamp: "2024-01-01T00:00:00Z",
|
||||||
|
request_id: "req-abc-123",
|
||||||
|
tool_session: "session-xyz-789",
|
||||||
|
execution_context: "test",
|
||||||
|
priority: "high",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle metadata parsing validation", async () => {
|
||||||
|
const server = createTestServerHttp({
|
||||||
|
serverInfo: createTestServerInfo(),
|
||||||
|
tools: [createEchoTool()],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.start("http");
|
||||||
|
const serverUrl = `${server.getUrl()}/mcp`;
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
serverUrl,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=parsing validation test",
|
||||||
|
"--metadata",
|
||||||
|
"valid_key=valid_value",
|
||||||
|
"numeric_key=123",
|
||||||
|
"boolean_key=true",
|
||||||
|
'json_key=\'{"test":"value"}\'',
|
||||||
|
"special_key=!@#$%^&*()",
|
||||||
|
"unicode_key=🚀🎉✨",
|
||||||
|
"--transport",
|
||||||
|
"http",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
|
||||||
|
// Validate all value types are sent as strings
|
||||||
|
// Note: The CLI parses metadata values, so single-quoted JSON strings
|
||||||
|
// are preserved with their quotes
|
||||||
|
const recordedRequests = server.getRecordedRequests();
|
||||||
|
const toolCallRequest = recordedRequests.find(
|
||||||
|
(r) => r.method === "tools/call",
|
||||||
|
);
|
||||||
|
expect(toolCallRequest).toBeDefined();
|
||||||
|
expect(toolCallRequest?.metadata).toEqual({
|
||||||
|
valid_key: "valid_value",
|
||||||
|
numeric_key: "123",
|
||||||
|
boolean_key: "true",
|
||||||
|
json_key: '\'{"test":"value"}\'', // Single quotes are preserved
|
||||||
|
special_key: "!@#$%^&*()",
|
||||||
|
unicode_key: "🚀🎉✨",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
523
cli/__tests__/tools.test.ts
Normal file
523
cli/__tests__/tools.test.ts
Normal file
|
|
@ -0,0 +1,523 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { runCli } from "./helpers/cli-runner.js";
|
||||||
|
import {
|
||||||
|
expectCliSuccess,
|
||||||
|
expectCliFailure,
|
||||||
|
expectValidJson,
|
||||||
|
expectJsonError,
|
||||||
|
} from "./helpers/assertions.js";
|
||||||
|
import { getTestMcpServerCommand } from "./helpers/test-server-stdio.js";
|
||||||
|
|
||||||
|
describe("Tool Tests", () => {
|
||||||
|
describe("Tool Discovery", () => {
|
||||||
|
it("should list available tools", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/list",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("tools");
|
||||||
|
expect(Array.isArray(json.tools)).toBe(true);
|
||||||
|
expect(json.tools.length).toBeGreaterThan(0);
|
||||||
|
// Validate that tools have required properties
|
||||||
|
expect(json.tools[0]).toHaveProperty("name");
|
||||||
|
expect(json.tools[0]).toHaveProperty("description");
|
||||||
|
// Validate expected tools from test-mcp-server
|
||||||
|
const toolNames = json.tools.map((tool: any) => tool.name);
|
||||||
|
expect(toolNames).toContain("echo");
|
||||||
|
expect(toolNames).toContain("get-sum");
|
||||||
|
expect(toolNames).toContain("get-annotated-message");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("JSON Argument Parsing", () => {
|
||||||
|
it("should handle string arguments (backward compatibility)", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=hello world",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
expect(json.content[0].text).toBe("Echo: hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle integer number arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-sum",
|
||||||
|
"--tool-arg",
|
||||||
|
"a=42",
|
||||||
|
"b=58",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// test-mcp-server returns JSON with {result: a+b}
|
||||||
|
const resultData = JSON.parse(json.content[0].text);
|
||||||
|
expect(resultData.result).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle decimal number arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-sum",
|
||||||
|
"--tool-arg",
|
||||||
|
"a=19.99",
|
||||||
|
"b=20.01",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// test-mcp-server returns JSON with {result: a+b}
|
||||||
|
const resultData = JSON.parse(json.content[0].text);
|
||||||
|
expect(resultData.result).toBeCloseTo(40.0, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle boolean arguments - true", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-annotated-message",
|
||||||
|
"--tool-arg",
|
||||||
|
"messageType=success",
|
||||||
|
"includeImage=true",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
// Should have both text and image content
|
||||||
|
expect(json.content.length).toBeGreaterThan(1);
|
||||||
|
const hasImage = json.content.some((item: any) => item.type === "image");
|
||||||
|
expect(hasImage).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle boolean arguments - false", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-annotated-message",
|
||||||
|
"--tool-arg",
|
||||||
|
"messageType=error",
|
||||||
|
"includeImage=false",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
// Should only have text content, no image
|
||||||
|
const hasImage = json.content.some((item: any) => item.type === "image");
|
||||||
|
expect(hasImage).toBe(false);
|
||||||
|
// test-mcp-server returns "This is a {messageType} message"
|
||||||
|
expect(json.content[0].text.toLowerCase()).toContain("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle null arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
'message="null"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// The string "null" should be passed through
|
||||||
|
expect(json.content[0].text).toBe("Echo: null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple arguments with mixed types", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-sum",
|
||||||
|
"--tool-arg",
|
||||||
|
"a=42.5",
|
||||||
|
"b=57.5",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// test-mcp-server returns JSON with {result: a+b}
|
||||||
|
const resultData = JSON.parse(json.content[0].text);
|
||||||
|
expect(resultData.result).toBeCloseTo(100.0, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("JSON Parsing Edge Cases", () => {
|
||||||
|
it("should fall back to string for invalid JSON", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message={invalid json}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Should treat invalid JSON as a string
|
||||||
|
expect(json.content[0].text).toBe("Echo: {invalid json}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty string value", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
'message=""',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Empty string should be preserved
|
||||||
|
expect(json.content[0].text).toBe("Echo: ");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle special characters in strings", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
'message="C:\\\\Users\\\\test"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Special characters should be preserved
|
||||||
|
expect(json.content[0].text).toContain("C:");
|
||||||
|
expect(json.content[0].text).toContain("Users");
|
||||||
|
expect(json.content[0].text).toContain("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle unicode characters", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
'message="🚀🎉✨"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Unicode characters should be preserved
|
||||||
|
expect(json.content[0].text).toContain("🚀");
|
||||||
|
expect(json.content[0].text).toContain("🎉");
|
||||||
|
expect(json.content[0].text).toContain("✨");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle arguments with equals signs in values", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=2+2=4",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Equals signs in values should be preserved
|
||||||
|
expect(json.content[0].text).toBe("Echo: 2+2=4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle base64-like strings", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const base64String =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=";
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
`message=${base64String}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// Base64-like strings should be preserved
|
||||||
|
expect(json.content[0].text).toBe(`Echo: ${base64String}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Tool Error Handling", () => {
|
||||||
|
it("should fail with nonexistent tool", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"nonexistent_tool",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// CLI returns exit code 0 but includes isError: true in JSON
|
||||||
|
expectJsonError(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when tool name is missing", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=test",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with invalid tool argument format", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"invalid_format_no_equals",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliFailure(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Prompt JSON Arguments", () => {
|
||||||
|
it("should handle prompt with JSON arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"args-prompt",
|
||||||
|
"--prompt-args",
|
||||||
|
"city=New York",
|
||||||
|
"state=NY",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("messages");
|
||||||
|
expect(Array.isArray(json.messages)).toBe(true);
|
||||||
|
expect(json.messages.length).toBeGreaterThan(0);
|
||||||
|
expect(json.messages[0]).toHaveProperty("content");
|
||||||
|
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||||
|
// Validate that the arguments were actually used in the response
|
||||||
|
// test-mcp-server formats it as "This is a prompt with arguments: city={city}, state={state}"
|
||||||
|
expect(json.messages[0].content.text).toContain("city=New York");
|
||||||
|
expect(json.messages[0].content.text).toContain("state=NY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle prompt with simple arguments", async () => {
|
||||||
|
// Note: simple-prompt doesn't accept arguments, but the CLI should still
|
||||||
|
// accept the command and the server should ignore the arguments
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"prompts/get",
|
||||||
|
"--prompt-name",
|
||||||
|
"simple-prompt",
|
||||||
|
"--prompt-args",
|
||||||
|
"name=test",
|
||||||
|
"count=5",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("messages");
|
||||||
|
expect(Array.isArray(json.messages)).toBe(true);
|
||||||
|
expect(json.messages.length).toBeGreaterThan(0);
|
||||||
|
expect(json.messages[0]).toHaveProperty("content");
|
||||||
|
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||||
|
// test-mcp-server's simple-prompt returns standard message (ignoring args)
|
||||||
|
expect(json.messages[0].content.text).toBe(
|
||||||
|
"This is a simple prompt for testing purposes.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Backward Compatibility", () => {
|
||||||
|
it("should support existing string-only usage", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"echo",
|
||||||
|
"--tool-arg",
|
||||||
|
"message=hello",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
expect(json.content[0].text).toBe("Echo: hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support multiple string arguments", async () => {
|
||||||
|
const { command, args } = getTestMcpServerCommand();
|
||||||
|
const result = await runCli([
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
"--cli",
|
||||||
|
"--method",
|
||||||
|
"tools/call",
|
||||||
|
"--tool-name",
|
||||||
|
"get-sum",
|
||||||
|
"--tool-arg",
|
||||||
|
"a=10",
|
||||||
|
"b=20",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectCliSuccess(result);
|
||||||
|
const json = expectValidJson(result);
|
||||||
|
expect(json).toHaveProperty("content");
|
||||||
|
expect(Array.isArray(json.content)).toBe(true);
|
||||||
|
expect(json.content.length).toBeGreaterThan(0);
|
||||||
|
expect(json.content[0]).toHaveProperty("type", "text");
|
||||||
|
// test-mcp-server returns JSON with {result: a+b}
|
||||||
|
const resultData = JSON.parse(json.content[0].text);
|
||||||
|
expect(resultData.result).toBe(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"name": "@modelcontextprotocol/inspector-cli",
|
"name": "@modelcontextprotocol/inspector-cli",
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"description": "CLI for the Model Context Protocol inspector",
|
"description": "CLI for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Model Context Protocol a Series of LF Projects, LLC.",
|
||||||
"homepage": "https://modelcontextprotocol.io",
|
"homepage": "https://modelcontextprotocol.io",
|
||||||
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
|
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
|
||||||
"main": "build/cli.js",
|
"main": "build/cli.js",
|
||||||
|
|
@ -17,15 +17,22 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"postbuild": "node scripts/make-executable.js",
|
"postbuild": "node scripts/make-executable.js",
|
||||||
"test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js",
|
"test": "vitest run",
|
||||||
"test:cli": "node scripts/cli-tests.js",
|
"test:watch": "vitest",
|
||||||
"test:cli-tools": "node scripts/cli-tool-tests.js",
|
"test:cli": "vitest run cli.test.ts",
|
||||||
"test:cli-headers": "node scripts/cli-header-tests.js"
|
"test:cli-tools": "vitest run tools.test.ts",
|
||||||
|
"test:cli-headers": "vitest run headers.test.ts",
|
||||||
|
"test:cli-metadata": "vitest run metadata.test.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"vitest": "^4.0.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
"spawn-rx": "^5.1.2"
|
"spawn-rx": "^5.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration tests for header functionality
|
|
||||||
* Tests the CLI header parsing end-to-end
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import { resolve, dirname } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const CLI_PATH = resolve(__dirname, "..", "build", "index.js");
|
|
||||||
|
|
||||||
// ANSI colors for output
|
|
||||||
const colors = {
|
|
||||||
GREEN: "\x1b[32m",
|
|
||||||
RED: "\x1b[31m",
|
|
||||||
YELLOW: "\x1b[33m",
|
|
||||||
BLUE: "\x1b[34m",
|
|
||||||
NC: "\x1b[0m", // No Color
|
|
||||||
};
|
|
||||||
|
|
||||||
let testsPassed = 0;
|
|
||||||
let testsFailed = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a CLI test with given arguments and check for expected behavior
|
|
||||||
*/
|
|
||||||
function runHeaderTest(
|
|
||||||
testName,
|
|
||||||
args,
|
|
||||||
expectSuccess = false,
|
|
||||||
expectedInOutput = null,
|
|
||||||
) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
console.log(`\n${colors.BLUE}Testing: ${testName}${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${CLI_PATH} ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const child = spawn("node", [CLI_PATH, ...args], {
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
stdout += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
stderr += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
const output = stdout + stderr;
|
|
||||||
let passed = true;
|
|
||||||
let reason = "";
|
|
||||||
|
|
||||||
// Check exit code expectation
|
|
||||||
if (expectSuccess && code !== 0) {
|
|
||||||
passed = false;
|
|
||||||
reason = `Expected success (exit code 0) but got ${code}`;
|
|
||||||
} else if (!expectSuccess && code === 0) {
|
|
||||||
passed = false;
|
|
||||||
reason = `Expected failure (non-zero exit code) but got success`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check expected output
|
|
||||||
if (passed && expectedInOutput && !output.includes(expectedInOutput)) {
|
|
||||||
passed = false;
|
|
||||||
reason = `Expected output to contain "${expectedInOutput}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passed) {
|
|
||||||
console.log(`${colors.GREEN}PASS: ${testName}${colors.NC}`);
|
|
||||||
testsPassed++;
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.RED}FAIL: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Reason: ${reason}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Exit code: ${code}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Output: ${output}${colors.NC}`);
|
|
||||||
testsFailed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("error", (error) => {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}ERROR: ${testName} - ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
testsFailed++;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runHeaderIntegrationTests() {
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}=== MCP Inspector CLI Header Integration Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Testing header parsing and validation${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 1: Valid header format should parse successfully (connection will fail)
|
|
||||||
await runHeaderTest(
|
|
||||||
"Valid single header",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
"Authorization: Bearer token123",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Multiple headers should parse successfully
|
|
||||||
await runHeaderTest(
|
|
||||||
"Multiple headers",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
"Authorization: Bearer token123",
|
|
||||||
"--header",
|
|
||||||
"X-API-Key: secret123",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Invalid header format - no colon
|
|
||||||
await runHeaderTest(
|
|
||||||
"Invalid header format - no colon",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
"InvalidHeader",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
"Invalid header format",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Invalid header format - empty name
|
|
||||||
await runHeaderTest(
|
|
||||||
"Invalid header format - empty name",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
": value",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
"Invalid header format",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5: Invalid header format - empty value
|
|
||||||
await runHeaderTest(
|
|
||||||
"Invalid header format - empty value",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
"Header:",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
"Invalid header format",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 6: Header with colons in value
|
|
||||||
await runHeaderTest(
|
|
||||||
"Header with colons in value",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
"X-Time: 2023:12:25:10:30:45",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 7: Whitespace handling
|
|
||||||
await runHeaderTest(
|
|
||||||
"Whitespace handling in headers",
|
|
||||||
[
|
|
||||||
"https://example.com",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--header",
|
|
||||||
" X-Header : value with spaces ",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}=== Test Results ===${colors.NC}`);
|
|
||||||
console.log(`${colors.GREEN}Tests passed: ${testsPassed}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Tests failed: ${testsFailed}${colors.NC}`);
|
|
||||||
|
|
||||||
if (testsFailed === 0) {
|
|
||||||
console.log(
|
|
||||||
`${colors.GREEN}All header integration tests passed!${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}Some header integration tests failed.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle graceful shutdown
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
console.log(`\n${colors.YELLOW}Test interrupted by user${colors.NC}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
console.log(`\n${colors.YELLOW}Test terminated${colors.NC}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run the tests
|
|
||||||
runHeaderIntegrationTests().catch((error) => {
|
|
||||||
console.error(`${colors.RED}Test runner error: ${error.message}${colors.NC}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,676 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Colors for output
|
|
||||||
const colors = {
|
|
||||||
GREEN: "\x1b[32m",
|
|
||||||
YELLOW: "\x1b[33m",
|
|
||||||
RED: "\x1b[31m",
|
|
||||||
BLUE: "\x1b[34m",
|
|
||||||
ORANGE: "\x1b[33m",
|
|
||||||
NC: "\x1b[0m", // No Color
|
|
||||||
};
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import os from "os";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
// Get directory paths with ESM compatibility
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Track test results
|
|
||||||
let PASSED_TESTS = 0;
|
|
||||||
let FAILED_TESTS = 0;
|
|
||||||
let SKIPPED_TESTS = 0;
|
|
||||||
let TOTAL_TESTS = 0;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}=== MCP Inspector CLI Metadata Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}This script tests the MCP Inspector CLI's metadata functionality:${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- General metadata with --metadata option${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Tool-specific metadata with --tool-metadata option${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Metadata parsing with various data types${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Metadata merging (tool-specific overrides general)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Metadata evaluation in different MCP methods${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`\n`);
|
|
||||||
|
|
||||||
// Get directory paths
|
|
||||||
const SCRIPTS_DIR = __dirname;
|
|
||||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
|
||||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
|
||||||
|
|
||||||
// Define the test server command using npx
|
|
||||||
const TEST_CMD = "npx";
|
|
||||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
|
||||||
|
|
||||||
// Create output directory for test results
|
|
||||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "metadata-test-output");
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary directory for test files
|
|
||||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-metadata-tests");
|
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
||||||
|
|
||||||
// Track servers for cleanup
|
|
||||||
let runningServers = [];
|
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
try {
|
|
||||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to run a basic test
|
|
||||||
async function runBasicTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
|
||||||
child.kill();
|
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
// Check if we got valid JSON output (indicating success) even if process didn't exit cleanly
|
|
||||||
const hasValidJsonOutput =
|
|
||||||
output.includes('"tools"') ||
|
|
||||||
output.includes('"resources"') ||
|
|
||||||
output.includes('"prompts"') ||
|
|
||||||
output.includes('"content"') ||
|
|
||||||
output.includes('"messages"') ||
|
|
||||||
output.includes('"contents"');
|
|
||||||
|
|
||||||
if (code === 0 || hasValidJsonOutput) {
|
|
||||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to run an error test (expected to fail)
|
|
||||||
async function runErrorTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
child.kill();
|
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
// For error tests, we expect a non-zero exit code
|
|
||||||
if (code !== 0) {
|
|
||||||
console.log(
|
|
||||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
async function runTests() {
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running General Metadata Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 1: General metadata with tools/list
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_tools_list",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: General metadata with resources/list
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_resources_list",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"resources/list",
|
|
||||||
"--metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: General metadata with prompts/list
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_prompts_list",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/list",
|
|
||||||
"--metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: General metadata with resources/read
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_resources_read",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"resources/read",
|
|
||||||
"--uri",
|
|
||||||
"test://static/resource/1",
|
|
||||||
"--metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5: General metadata with prompts/get
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_prompts_get",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"simple_prompt",
|
|
||||||
"--metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Tool-Specific Metadata Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 6: Tool-specific metadata with tools/call
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_tools_call_tool_meta",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=hello world",
|
|
||||||
"--tool-metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 7: Tool-specific metadata with complex tool
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_tools_call_complex_tool_meta",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"add",
|
|
||||||
"--tool-arg",
|
|
||||||
"a=10",
|
|
||||||
"b=20",
|
|
||||||
"--tool-metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Merging Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 8: General metadata + tool-specific metadata (tool-specific should override)
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_merging_general_and_tool",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=hello world",
|
|
||||||
"--metadata",
|
|
||||||
"client=general-client",
|
|
||||||
"--tool-metadata",
|
|
||||||
"client=test-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Parsing Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 10: Metadata with numeric values (should be converted to strings)
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_parsing_numbers",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"integer_value=42",
|
|
||||||
"decimal_value=3.14159",
|
|
||||||
"negative_value=-10",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 11: Metadata with JSON values (should be converted to strings)
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_parsing_json",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
'json_object="{\\"key\\":\\"value\\"}"',
|
|
||||||
'json_array="[1,2,3]"',
|
|
||||||
'json_string="\\"quoted\\""',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 12: Metadata with special characters
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_parsing_special_chars",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"unicode=🚀🎉✨",
|
|
||||||
"special_chars=!@#$%^&*()",
|
|
||||||
"spaces=hello world with spaces",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Edge Cases ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 13: Single metadata entry
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_single_entry",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"single_key=single_value",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 14: Many metadata entries
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_many_entries",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"key1=value1",
|
|
||||||
"key2=value2",
|
|
||||||
"key3=value3",
|
|
||||||
"key4=value4",
|
|
||||||
"key5=value5",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Error Cases ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 15: Invalid metadata format (missing equals)
|
|
||||||
await runErrorTest(
|
|
||||||
"metadata_error_invalid_format",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"invalid_format_no_equals",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 16: Invalid tool-meta format (missing equals)
|
|
||||||
await runErrorTest(
|
|
||||||
"metadata_error_invalid_tool_meta_format",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=test",
|
|
||||||
"--tool-metadata",
|
|
||||||
"invalid_format_no_equals",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Impact Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 17: Test tool-specific metadata vs general metadata precedence
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_precedence_tool_overrides_general",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=precedence test",
|
|
||||||
"--metadata",
|
|
||||||
"client=general-client",
|
|
||||||
"--tool-metadata",
|
|
||||||
"client=tool-specific-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 18: Test metadata with resources methods
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_resources_methods",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"resources/list",
|
|
||||||
"--metadata",
|
|
||||||
"resource_client=test-resource-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 19: Test metadata with prompts methods
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_prompts_methods",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"simple_prompt",
|
|
||||||
"--metadata",
|
|
||||||
"prompt_client=test-prompt-client",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Validation Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 20: Test metadata with special characters in keys
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_special_key_characters",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=special keys test",
|
|
||||||
"--metadata",
|
|
||||||
"key-with-dashes=value1",
|
|
||||||
"key_with_underscores=value2",
|
|
||||||
"key.with.dots=value3",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Metadata Integration Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 21: Metadata with all MCP methods
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_integration_all_methods",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
"--metadata",
|
|
||||||
"integration_test=true",
|
|
||||||
"test_phase=all_methods",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 22: Complex metadata scenario
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_complex_scenario",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=complex test",
|
|
||||||
"--metadata",
|
|
||||||
"session_id=12345",
|
|
||||||
"user_id=67890",
|
|
||||||
"timestamp=2024-01-01T00:00:00Z",
|
|
||||||
"request_id=req-abc-123",
|
|
||||||
"--tool-metadata",
|
|
||||||
"tool_session=session-xyz-789",
|
|
||||||
"execution_context=test",
|
|
||||||
"priority=high",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 23: Metadata parsing validation test
|
|
||||||
await runBasicTest(
|
|
||||||
"metadata_parsing_validation",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=parsing validation test",
|
|
||||||
"--metadata",
|
|
||||||
"valid_key=valid_value",
|
|
||||||
"numeric_key=123",
|
|
||||||
"boolean_key=true",
|
|
||||||
'json_key=\'{"test":"value"}\'',
|
|
||||||
"special_key=!@#$%^&*()",
|
|
||||||
"unicode_key=🚀🎉✨",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Print test summary
|
|
||||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
|
||||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`Total: ${TOTAL_TESTS}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.GREEN}All metadata tests completed!${colors.NC}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
runTests().catch((error) => {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,935 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Colors for output
|
|
||||||
const colors = {
|
|
||||||
GREEN: "\x1b[32m",
|
|
||||||
YELLOW: "\x1b[33m",
|
|
||||||
RED: "\x1b[31m",
|
|
||||||
BLUE: "\x1b[34m",
|
|
||||||
ORANGE: "\x1b[33m",
|
|
||||||
NC: "\x1b[0m", // No Color
|
|
||||||
};
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import os from "os";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
// Get directory paths with ESM compatibility
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Track test results
|
|
||||||
let PASSED_TESTS = 0;
|
|
||||||
let FAILED_TESTS = 0;
|
|
||||||
let SKIPPED_TESTS = 0;
|
|
||||||
let TOTAL_TESTS = 0;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}=== MCP Inspector CLI Test Script ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}This script tests the MCP Inspector CLI's ability to handle various command line options:${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}- Basic CLI mode${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}- Environment variables (-e)${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}- Config file (--config)${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}- Server selection (--server)${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}- Method selection (--method)${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`\n`);
|
|
||||||
|
|
||||||
// Get directory paths
|
|
||||||
const SCRIPTS_DIR = __dirname;
|
|
||||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
|
||||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
|
||||||
|
|
||||||
// Define the test server command using npx
|
|
||||||
const TEST_CMD = "npx";
|
|
||||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
|
||||||
|
|
||||||
// Create output directory for test results
|
|
||||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "test-output");
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary directory for test files
|
|
||||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests");
|
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
||||||
|
|
||||||
// Track servers for cleanup
|
|
||||||
let runningServers = [];
|
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
try {
|
|
||||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use the existing sample config file
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Using existing sample config file: ${PROJECT_ROOT}/sample-config.json${colors.NC}`,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const sampleConfig = fs.readFileSync(
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
console.log(sampleConfig);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error reading sample config: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an invalid config file for testing
|
|
||||||
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
|
|
||||||
fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {');
|
|
||||||
|
|
||||||
// Create config files with different transport types for testing
|
|
||||||
const sseConfigPath = path.join(TEMP_DIR, "sse-config.json");
|
|
||||||
fs.writeFileSync(
|
|
||||||
sseConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"test-sse": {
|
|
||||||
type: "sse",
|
|
||||||
url: "http://localhost:3000/sse",
|
|
||||||
note: "Test SSE server",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const httpConfigPath = path.join(TEMP_DIR, "http-config.json");
|
|
||||||
fs.writeFileSync(
|
|
||||||
httpConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"test-http": {
|
|
||||||
type: "streamable-http",
|
|
||||||
url: "http://localhost:3000/mcp",
|
|
||||||
note: "Test HTTP server",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json");
|
|
||||||
fs.writeFileSync(
|
|
||||||
stdioConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"test-stdio": {
|
|
||||||
type: "stdio",
|
|
||||||
command: "npx",
|
|
||||||
args: ["@modelcontextprotocol/server-everything"],
|
|
||||||
env: {
|
|
||||||
TEST_ENV: "test-value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Config without type field (backward compatibility)
|
|
||||||
const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json");
|
|
||||||
fs.writeFileSync(
|
|
||||||
legacyConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"test-legacy": {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@modelcontextprotocol/server-everything"],
|
|
||||||
env: {
|
|
||||||
LEGACY_ENV: "legacy-value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Function to run a basic test
|
|
||||||
async function runBasicTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
|
||||||
child.kill();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to run an error test (expected to fail)
|
|
||||||
async function runErrorTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
child.kill();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
// For error tests, we expect a non-zero exit code
|
|
||||||
if (code !== 0) {
|
|
||||||
console.log(
|
|
||||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
async function runTests() {
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Basic CLI Mode Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 1: Basic CLI mode with method
|
|
||||||
await runBasicTest(
|
|
||||||
"basic_cli_mode",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: CLI mode with non-existent method (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"nonexistent_method",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"nonexistent/method",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: CLI mode without method (should fail)
|
|
||||||
await runErrorTest("missing_method", TEST_CMD, ...TEST_ARGS, "--cli");
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Environment Variable Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: CLI mode with environment variables
|
|
||||||
await runBasicTest(
|
|
||||||
"env_variables",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"-e",
|
|
||||||
"KEY1=value1",
|
|
||||||
"-e",
|
|
||||||
"KEY2=value2",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5: CLI mode with invalid environment variable format (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"invalid_env_format",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"-e",
|
|
||||||
"INVALID_FORMAT",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5b: CLI mode with environment variable containing equals sign in value
|
|
||||||
await runBasicTest(
|
|
||||||
"env_variable_with_equals",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"-e",
|
|
||||||
"API_KEY=abc123=xyz789==",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5c: CLI mode with environment variable containing base64-encoded value
|
|
||||||
await runBasicTest(
|
|
||||||
"env_variable_with_base64",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"-e",
|
|
||||||
"JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Config File Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 6: Using config file with CLI mode
|
|
||||||
await runBasicTest(
|
|
||||||
"config_file",
|
|
||||||
"--config",
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 7: Using config file without server name (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"config_without_server",
|
|
||||||
"--config",
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 8: Using server name without config file (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"server_without_config",
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 9: Using non-existent config file (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"nonexistent_config",
|
|
||||||
"--config",
|
|
||||||
"./nonexistent-config.json",
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 10: Using invalid config file format (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"invalid_config",
|
|
||||||
"--config",
|
|
||||||
invalidConfigPath,
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 11: Using config file with non-existent server (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"nonexistent_server",
|
|
||||||
"--config",
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"--server",
|
|
||||||
"nonexistent",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 16: CLI mode with resource read
|
|
||||||
await runBasicTest(
|
|
||||||
"resource_read",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"resources/read",
|
|
||||||
"--uri",
|
|
||||||
"test://static/resource/1",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 17: CLI mode with resource read but missing URI (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"missing_uri",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"resources/read",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Prompt-Related Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 18: CLI mode with prompt get
|
|
||||||
await runBasicTest(
|
|
||||||
"prompt_get",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"simple_prompt",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 19: CLI mode with prompt get and args
|
|
||||||
await runBasicTest(
|
|
||||||
"prompt_get_with_args",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"complex_prompt",
|
|
||||||
"--prompt-args",
|
|
||||||
"temperature=0.7",
|
|
||||||
"style=concise",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 20: CLI mode with prompt get but missing prompt name (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"missing_prompt_name",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}=== Running Logging Tests ===${colors.NC}`);
|
|
||||||
|
|
||||||
// Test 21: CLI mode with log level
|
|
||||||
await runBasicTest(
|
|
||||||
"log_level",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"logging/setLevel",
|
|
||||||
"--log-level",
|
|
||||||
"debug",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 22: CLI mode with invalid log level (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"invalid_log_level",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"logging/setLevel",
|
|
||||||
"--log-level",
|
|
||||||
"invalid",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Combined Option Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note about the combined options issue
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Testing combined options with environment variables and config file.${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 23: CLI mode with config file, environment variables, and tool call
|
|
||||||
await runBasicTest(
|
|
||||||
"combined_options",
|
|
||||||
"--config",
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"-e",
|
|
||||||
"CLI_ENV_VAR=cli_value",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 24: CLI mode with all possible options (that make sense together)
|
|
||||||
await runBasicTest(
|
|
||||||
"all_options",
|
|
||||||
"--config",
|
|
||||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
|
||||||
"--server",
|
|
||||||
"everything",
|
|
||||||
"-e",
|
|
||||||
"CLI_ENV_VAR=cli_value",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=Hello",
|
|
||||||
"--log-level",
|
|
||||||
"debug",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 25: Config with stdio transport type
|
|
||||||
await runBasicTest(
|
|
||||||
"config_stdio_type",
|
|
||||||
"--config",
|
|
||||||
stdioConfigPath,
|
|
||||||
"--server",
|
|
||||||
"test-stdio",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 26: Config with SSE transport type (CLI mode) - expects connection error
|
|
||||||
await runErrorTest(
|
|
||||||
"config_sse_type_cli",
|
|
||||||
"--config",
|
|
||||||
sseConfigPath,
|
|
||||||
"--server",
|
|
||||||
"test-sse",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 27: Config with streamable-http transport type (CLI mode) - expects connection error
|
|
||||||
await runErrorTest(
|
|
||||||
"config_http_type_cli",
|
|
||||||
"--config",
|
|
||||||
httpConfigPath,
|
|
||||||
"--server",
|
|
||||||
"test-http",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 28: Legacy config without type field (backward compatibility)
|
|
||||||
await runBasicTest(
|
|
||||||
"config_legacy_no_type",
|
|
||||||
"--config",
|
|
||||||
legacyConfigPath,
|
|
||||||
"--server",
|
|
||||||
"test-legacy",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create config with single server for auto-selection
|
|
||||||
const singleServerConfigPath = path.join(
|
|
||||||
TEMP_DIR,
|
|
||||||
"single-server-config.json",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
singleServerConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"only-server": {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@modelcontextprotocol/server-everything"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create config with default-server
|
|
||||||
const defaultServerConfigPath = path.join(
|
|
||||||
TEMP_DIR,
|
|
||||||
"default-server-config.json",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
defaultServerConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
"default-server": {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@modelcontextprotocol/server-everything"],
|
|
||||||
},
|
|
||||||
"other-server": {
|
|
||||||
command: "node",
|
|
||||||
args: ["other.js"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create config with multiple servers (no default)
|
|
||||||
const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json");
|
|
||||||
fs.writeFileSync(
|
|
||||||
multiServerConfigPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
mcpServers: {
|
|
||||||
server1: {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@modelcontextprotocol/server-everything"],
|
|
||||||
},
|
|
||||||
server2: {
|
|
||||||
command: "node",
|
|
||||||
args: ["other.js"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 29: Config with single server auto-selection
|
|
||||||
await runBasicTest(
|
|
||||||
"single_server_auto_select",
|
|
||||||
"--config",
|
|
||||||
singleServerConfigPath,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 30: Config with default-server should now require explicit selection (multiple servers)
|
|
||||||
await runErrorTest(
|
|
||||||
"default_server_requires_explicit_selection",
|
|
||||||
"--config",
|
|
||||||
defaultServerConfigPath,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 31: Config with multiple servers and no default (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"multi_server_no_default",
|
|
||||||
"--config",
|
|
||||||
multiServerConfigPath,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`,
|
|
||||||
);
|
|
||||||
const httpServer = spawn(
|
|
||||||
"npx",
|
|
||||||
["@modelcontextprotocol/server-everything", "streamableHttp"],
|
|
||||||
{
|
|
||||||
detached: true,
|
|
||||||
stdio: "ignore",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
runningServers.push(httpServer);
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
||||||
|
|
||||||
// Test 32: HTTP transport inferred from URL ending with /mcp
|
|
||||||
await runBasicTest(
|
|
||||||
"http_transport_inferred",
|
|
||||||
"http://127.0.0.1:3001/mcp",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 33: HTTP transport with explicit --transport http flag
|
|
||||||
await runBasicTest(
|
|
||||||
"http_transport_with_explicit_flag",
|
|
||||||
"http://127.0.0.1:3001/mcp",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 34: HTTP transport with suffix and --transport http flag
|
|
||||||
await runBasicTest(
|
|
||||||
"http_transport_with_explicit_flag_and_suffix",
|
|
||||||
"http://127.0.0.1:3001/mcp",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 35: SSE transport given to HTTP server (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"sse_transport_given_to_http_server",
|
|
||||||
"http://127.0.0.1:3001",
|
|
||||||
"--transport",
|
|
||||||
"sse",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 36: HTTP transport without URL (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"http_transport_without_url",
|
|
||||||
"--transport",
|
|
||||||
"http",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 37: SSE transport without URL (should fail)
|
|
||||||
await runErrorTest(
|
|
||||||
"sse_transport_without_url",
|
|
||||||
"--transport",
|
|
||||||
"sse",
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Kill HTTP server
|
|
||||||
try {
|
|
||||||
process.kill(-httpServer.pid);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print test summary
|
|
||||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
|
||||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`Total: ${TOTAL_TESTS}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.GREEN}All tests completed!${colors.NC}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
runTests().catch((error) => {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,614 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Colors for output
|
|
||||||
const colors = {
|
|
||||||
GREEN: "\x1b[32m",
|
|
||||||
YELLOW: "\x1b[33m",
|
|
||||||
RED: "\x1b[31m",
|
|
||||||
BLUE: "\x1b[34m",
|
|
||||||
ORANGE: "\x1b[33m",
|
|
||||||
NC: "\x1b[0m", // No Color
|
|
||||||
};
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import os from "os";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
// Get directory paths with ESM compatibility
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Track test results
|
|
||||||
let PASSED_TESTS = 0;
|
|
||||||
let FAILED_TESTS = 0;
|
|
||||||
let SKIPPED_TESTS = 0;
|
|
||||||
let TOTAL_TESTS = 0;
|
|
||||||
|
|
||||||
console.log(`${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Tool execution with various argument types${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`\n`);
|
|
||||||
|
|
||||||
// Get directory paths
|
|
||||||
const SCRIPTS_DIR = __dirname;
|
|
||||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
|
||||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
|
||||||
|
|
||||||
// Define the test server command using npx
|
|
||||||
const TEST_CMD = "npx";
|
|
||||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
|
||||||
|
|
||||||
// Create output directory for test results
|
|
||||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "tool-test-output");
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary directory for test files
|
|
||||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tool-tests");
|
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
||||||
|
|
||||||
// Track servers for cleanup
|
|
||||||
let runningServers = [];
|
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
try {
|
|
||||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
runningServers.forEach((server) => {
|
|
||||||
try {
|
|
||||||
process.kill(-server.pid);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to run a basic test
|
|
||||||
async function runBasicTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
|
||||||
child.kill();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to run an error test (expected to fail)
|
|
||||||
async function runErrorTest(testName, ...args) {
|
|
||||||
const outputFile = path.join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
`${testName.replace(/\//g, "_")}.log`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
|
||||||
TOTAL_TESTS++;
|
|
||||||
|
|
||||||
// Run the command and capture output
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create a write stream for the output file
|
|
||||||
const outputStream = fs.createWriteStream(outputFile);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
child.kill();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// Pipe stdout and stderr to the output file
|
|
||||||
child.stdout.pipe(outputStream);
|
|
||||||
child.stderr.pipe(outputStream);
|
|
||||||
|
|
||||||
// Also capture output for display
|
|
||||||
let output = "";
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
outputStream.end();
|
|
||||||
|
|
||||||
// For error tests, we expect a non-zero exit code
|
|
||||||
if (code !== 0) {
|
|
||||||
console.log(
|
|
||||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
|
||||||
const firstFewLines = output
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
console.log(firstFewLines);
|
|
||||||
PASSED_TESTS++;
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
|
||||||
);
|
|
||||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
|
||||||
console.log(
|
|
||||||
output
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => ` ${line}`)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
|
|
||||||
// Stop after any error is encountered
|
|
||||||
console.log(
|
|
||||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
FAILED_TESTS++;
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
async function runTests() {
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Tool Discovery Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 1: List available tools
|
|
||||||
await runBasicTest(
|
|
||||||
"tool_discovery_list",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/list",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running JSON Argument Parsing Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: String arguments (backward compatibility)
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_string",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=hello world",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Number arguments
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_number_integer",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"add",
|
|
||||||
"--tool-arg",
|
|
||||||
"a=42",
|
|
||||||
"b=58",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Number arguments with decimals (using add tool with decimal numbers)
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_number_decimal",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"add",
|
|
||||||
"--tool-arg",
|
|
||||||
"a=19.99",
|
|
||||||
"b=20.01",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 5: Boolean arguments - true
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_boolean_true",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"annotatedMessage",
|
|
||||||
"--tool-arg",
|
|
||||||
"messageType=success",
|
|
||||||
"includeImage=true",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 6: Boolean arguments - false
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_boolean_false",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"annotatedMessage",
|
|
||||||
"--tool-arg",
|
|
||||||
"messageType=error",
|
|
||||||
"includeImage=false",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 7: Null arguments (using echo with string "null")
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_null",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
'message="null"',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 14: Multiple arguments with mixed types (using add tool)
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_multiple_mixed",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"add",
|
|
||||||
"--tool-arg",
|
|
||||||
"a=42.5",
|
|
||||||
"b=57.5",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running JSON Parsing Edge Cases ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 15: Invalid JSON should fall back to string
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_invalid_fallback",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message={invalid json}",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 16: Empty string value
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_empty_value",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
'message=""',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 17: Special characters in strings
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_special_chars",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
'message="C:\\\\Users\\\\test"',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 18: Unicode characters
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_unicode",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
'message="🚀🎉✨"',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 19: Arguments with equals signs in values
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_equals_in_value",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=2+2=4",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 20: Base64-like strings
|
|
||||||
await runBasicTest(
|
|
||||||
"json_args_base64_like",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Tool Error Handling Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 21: Non-existent tool
|
|
||||||
await runErrorTest(
|
|
||||||
"tool_error_nonexistent",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"nonexistent_tool",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=test",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 22: Missing tool name
|
|
||||||
await runErrorTest(
|
|
||||||
"tool_error_missing_name",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=test",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 23: Invalid tool argument format
|
|
||||||
await runErrorTest(
|
|
||||||
"tool_error_invalid_arg_format",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"invalid_format_no_equals",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Prompt JSON Argument Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 24: Prompt with JSON arguments
|
|
||||||
await runBasicTest(
|
|
||||||
"prompt_json_args_mixed",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"complex_prompt",
|
|
||||||
"--prompt-args",
|
|
||||||
"temperature=0.7",
|
|
||||||
'style="concise"',
|
|
||||||
'options={"format":"json","max_tokens":100}',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 25: Prompt with simple arguments
|
|
||||||
await runBasicTest(
|
|
||||||
"prompt_json_args_simple",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"prompts/get",
|
|
||||||
"--prompt-name",
|
|
||||||
"simple_prompt",
|
|
||||||
"--prompt-args",
|
|
||||||
"name=test",
|
|
||||||
"count=5",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n${colors.YELLOW}=== Running Backward Compatibility Tests ===${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 26: Ensure existing string-only usage still works
|
|
||||||
await runBasicTest(
|
|
||||||
"backward_compatibility_strings",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"echo",
|
|
||||||
"--tool-arg",
|
|
||||||
"message=hello",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 27: Multiple string arguments (existing pattern) - using add tool
|
|
||||||
await runBasicTest(
|
|
||||||
"backward_compatibility_multiple_strings",
|
|
||||||
TEST_CMD,
|
|
||||||
...TEST_ARGS,
|
|
||||||
"--cli",
|
|
||||||
"--method",
|
|
||||||
"tools/call",
|
|
||||||
"--tool-name",
|
|
||||||
"add",
|
|
||||||
"--tool-arg",
|
|
||||||
"a=10",
|
|
||||||
"b=20",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Print test summary
|
|
||||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
|
||||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
|
||||||
console.log(`Total: ${TOTAL_TESTS}`);
|
|
||||||
console.log(
|
|
||||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`\n${colors.GREEN}All tool tests completed!${colors.NC}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
runTests().catch((error) => {
|
|
||||||
console.error(
|
|
||||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
10
cli/vitest.config.ts
Normal file
10
cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
include: ["**/__tests__/**/*.test.ts"],
|
||||||
|
testTimeout: 15000, // 15 seconds - CLI tests spawn subprocesses that need time
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"description": "Client-side application for the Model Context Protocol inspector",
|
"description": "Client-side application for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Model Context Protocol a Series of LF Projects, LLC.",
|
||||||
"homepage": "https://modelcontextprotocol.io",
|
"homepage": "https://modelcontextprotocol.io",
|
||||||
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
|
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue