Merge branch 'main' into fix/enum-anyof-ref-resolution
This commit is contained in:
commit
442638f4c1
71 changed files with 16245 additions and 8744 deletions
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -34,3 +34,7 @@ If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
**Version Consideration**
|
||||||
|
|
||||||
|
Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for more details.
|
||||||
|
|
|
||||||
57
.github/pull_request_template.md
vendored
Normal file
57
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- Provide a brief description of what this PR does -->
|
||||||
|
|
||||||
|
> **Note:** Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds functionality)
|
||||||
|
- [ ] Documentation update
|
||||||
|
- [ ] Refactoring (no functional changes)
|
||||||
|
- [ ] Test updates
|
||||||
|
- [ ] Build/CI improvements
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
<!-- Describe the changes in detail. Include screenshots/recordings if applicable -->
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
<!-- Link to related issues using #issue_number or "Fixes #issue_number" -->
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
<!-- Describe how you tested these changes, where applicable -->
|
||||||
|
|
||||||
|
- [ ] Tested in UI mode
|
||||||
|
- [ ] Tested in CLI mode
|
||||||
|
- [ ] Tested with STDIO transport
|
||||||
|
- [ ] Tested with SSE transport
|
||||||
|
- [ ] Tested with Streamable HTTP transport
|
||||||
|
- [ ] Added/updated automated tests
|
||||||
|
- [ ] Manual testing performed
|
||||||
|
|
||||||
|
### Test Results and/or Instructions
|
||||||
|
|
||||||
|
<!-- Provide steps for reviewers to test your changes -->
|
||||||
|
|
||||||
|
Screenshots are encouraged to share your testing results for this change.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Code follows the style guidelines (ran `npm run prettier-fix`)
|
||||||
|
- [ ] Self-review completed
|
||||||
|
- [ ] Code is commented where necessary
|
||||||
|
- [ ] Documentation updated (README, comments, etc.)
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
<!-- If this is a breaking change, describe the impact and migration path -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Add any other context, screenshots, or information about the PR here -->
|
||||||
50
.github/workflows/claude.yml
vendored
50
.github/workflows/claude.yml
vendored
|
|
@ -27,14 +27,48 @@ jobs:
|
||||||
id-token: write
|
id-token: write
|
||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Get PR details
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
|
||||||
|
github.event_name == 'pull_request_review_comment' ||
|
||||||
|
github.event_name == 'pull_request_review'
|
||||||
|
id: pr
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let prNumber;
|
||||||
|
if (context.eventName === 'issue_comment') {
|
||||||
|
prNumber = context.issue.number;
|
||||||
|
} else {
|
||||||
|
prNumber = context.payload.pull_request.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = await github.rest.pulls.get({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
core.setOutput('sha', pr.data.head.sha);
|
||||||
|
core.setOutput('repo', pr.data.head.repo.full_name);
|
||||||
|
|
||||||
|
- name: Checkout PR branch
|
||||||
|
if: steps.pr.outcome == 'success'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
ref: ${{ steps.pr.outputs.sha }}
|
||||||
|
repository: ${{ steps.pr.outputs.repo }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
if: steps.pr.outcome != 'success'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude
|
id: claude
|
||||||
uses: anthropics/claude-code-action@beta
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|
||||||
|
|
@ -45,9 +79,7 @@ jobs:
|
||||||
# Trigger when assigned to an issue
|
# Trigger when assigned to an issue
|
||||||
assignee_trigger: "claude"
|
assignee_trigger: "claude"
|
||||||
|
|
||||||
# Allow Claude to run bash
|
claude_args: |
|
||||||
# This should be safe given the repo is already public
|
--mcp-config .mcp.json
|
||||||
allowed_tools: "Bash"
|
--allowedTools "Bash,mcp__mcp-docs"
|
||||||
|
--append-system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block. When working on MCP-related code or reviewing MCP-related changes, use the mcp-docs MCP server to look up the latest protocol documentation. For schema details, reference https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema which contains versioned schemas in JSON (schema.json) and TypeScript (schema.ts) formats."
|
||||||
custom_instructions: |
|
|
||||||
If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block.
|
|
||||||
|
|
|
||||||
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:
|
||||||
|
|
|
||||||
2
.github/workflows/e2e_tests.yml
vendored
2
.github/workflows/e2e_tests.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
# Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load.
|
# Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load.
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ client/tsconfig.node.tsbuildinfo
|
||||||
cli/build
|
cli/build
|
||||||
test-output
|
test-output
|
||||||
tool-test-output
|
tool-test-output
|
||||||
|
metadata-test-output
|
||||||
# symlinked by `npm run link:sdk`:
|
# symlinked by `npm run link:sdk`:
|
||||||
sdk
|
sdk
|
||||||
client/playwright-report/
|
client/playwright-report/
|
||||||
|
|
|
||||||
8
.mcp.json
Normal file
8
.mcp.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mcp-docs": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://modelcontextprotocol.io/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# MCP Inspector Development Guide
|
# MCP Inspector Development Guide
|
||||||
|
|
||||||
|
> **Note:** Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
- Build all: `npm run build`
|
- Build all: `npm run build`
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,16 @@ Thanks for your interest in contributing! This guide explains how to get involve
|
||||||
3. Run `npm run dev` to start both client and server in development mode
|
3. Run `npm run dev` to start both client and server in development mode
|
||||||
4. Use the web UI at http://localhost:6274 to interact with the inspector
|
4. Use the web UI at http://localhost:6274 to interact with the inspector
|
||||||
|
|
||||||
|
## Inspector V2 Development
|
||||||
|
|
||||||
|
We're actively developing **Inspector V2** to address architectural and UX improvements. We invite you to follow progress and participate in the Inspector V2 Working Group in [Discord](https://modelcontextprotocol.io/community/communication), [weekly meetings](https://meet.modelcontextprotocol.io/tag/inspector-v2-wg), and [GitHub Discussions](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/categories/meeting-notes-other) (where notes are posted after meetings).
|
||||||
|
|
||||||
|
**Current version (V1) contribution scope:**
|
||||||
|
|
||||||
|
- Bug fixes and MCP spec compliance are actively maintained
|
||||||
|
- Documentation updates are always appreciated
|
||||||
|
- Major changes will be directed to V2 development
|
||||||
|
|
||||||
## Development Process & Pull Requests
|
## Development Process & Pull Requests
|
||||||
|
|
||||||
1. Create a new branch for your changes
|
1. Create a new branch for your changes
|
||||||
|
|
@ -30,7 +40,7 @@ If you find a security vulnerability, please refer to our [Security Policy](SECU
|
||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics.
|
Feel free to [open an issue](https://github.com/modelcontextprotocol/inspector/issues) for questions or join the MCP Contributor [Discord server](https://modelcontextprotocol.io/community/communication). Also, please see notes above on Inspector V2 Development.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,12 @@ The server will start up and the UI will be accessible at `http://localhost:6274
|
||||||
You can also start it in a Docker container with the following command:
|
You can also start it in a Docker container with the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest
|
docker run --rm \
|
||||||
|
-p 127.0.0.1:6274:6274 \
|
||||||
|
-p 127.0.0.1:6277:6277 \
|
||||||
|
-e HOST=0.0.0.0 \
|
||||||
|
-e MCP_AUTO_OPEN_ENABLED=false \
|
||||||
|
ghcr.io/modelcontextprotocol/inspector:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### From an MCP server repository
|
### From an MCP server repository
|
||||||
|
|
|
||||||
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue