Compare commits

...

142 Commits

Author SHA1 Message Date
Ashwin Bhat
3110cf9343 Merge pull request #88 from modelcontextprotocol/ani/fix-npx
Enable using 'npx' as your command on Windows
2024-11-27 16:03:37 -05:00
Ani Betts
2c04fa31e8 Merge branch 'main' into ani/fix-npx 2024-11-27 21:57:44 +01:00
Ashwin Bhat
e700bc713a Merge pull request #87 from modelcontextprotocol/ashwin/versiondisplay
display inspector version in UI
2024-11-27 13:06:52 -05:00
Ashwin Bhat
bea86af65b Merge pull request #89 from evalstate/main
Dark Mode and Word Wrap for Resource Viewer
2024-11-27 13:06:06 -05:00
evalstate
68a6130b17 fix dark mode styling and add word wrap for resource viewer. 2024-11-27 17:56:22 +00:00
Ani Betts
853a3b4faf Enable using 'npx' as your command on Windows 2024-11-27 17:04:52 +01:00
Ashwin Bhat
6f62066d34 display inspector version in UI 2024-11-27 10:52:38 -05:00
ashwin-ant
c770d217e7 Merge pull request #86 from modelcontextprotocol/ani/debuggability
Make debugging Inspector easier for users
2024-11-27 10:04:00 -05:00
Ani Betts
98470a12f9 Make stdout/error echo for client and server 2024-11-27 15:57:02 +01:00
Ani Betts
a00564fafa Disable minification on production build, we don't need it here and it makes debugging annoying 2024-11-27 15:55:11 +01:00
Ashwin Bhat
62546dec58 bump version to 0.2.4 2024-11-27 09:33:15 -05:00
ashwin-ant
886ac5fc7b Merge pull request #81 from modelcontextprotocol/ashwin/serverport
Respect custom server port
2024-11-27 08:59:59 -05:00
ashwin-ant
722df4d798 Merge pull request #82 from jacksteamdev/fix-1
Add Runtime Type Validation for Tool Results
2024-11-26 15:32:58 -05:00
Jack Steam
407e304585 Merge branch 'main' into fix-1 2024-11-26 13:31:12 -07:00
Jack Steam
60578314aa Update client/src/components/ToolsTab.tsx
Co-authored-by: ashwin-ant <ashwin@anthropic.com>
2024-11-26 13:30:43 -07:00
Ashwin Bhat
3c4cb17d09 bump to 0.2.3 2024-11-26 14:14:19 -05:00
Jack Steam
fbac5b78bc feat: add data validation message 2024-11-26 12:47:39 -06:00
Ashwin Bhat
f876b1ec0d consolidate server URL configuration 2024-11-26 13:40:28 -05:00
Jack Steam
aecfa21d47 fix: add static type validation 2024-11-26 11:14:55 -07:00
Ashwin Bhat
a3d542c0a3 make server port configurable via URL query param 2024-11-26 13:12:45 -05:00
Ani Betts
2b79b6ffd4 Merge pull request #79 from modelcontextprotocol/ani/fix-windows
Fix launch issues on Windows
2024-11-26 18:12:48 +01:00
Ani Betts
1f28b4474c Don't eat the env, add PORT 2024-11-26 18:08:43 +01:00
Anaïs Betts
d69d67cb64 Fix server args 2024-11-26 17:33:22 +01:00
Anaïs Betts
7792070d81 Add debugging 2024-11-26 17:18:33 +01:00
Anaïs Betts
34a2843756 resolve usually better than join 2024-11-26 17:11:32 +01:00
Anaïs Betts
2a34770959 Don't 🔥 the hello message 2024-11-26 17:06:56 +01:00
Anaïs Betts
6b674b0827 Version bump MCP SDK to latest 2024-11-26 17:02:39 +01:00
Anaïs Betts
ca8db1f417 Handle spawning the client and server on Windows using correct paths 2024-11-26 17:02:39 +01:00
Anaïs Betts
eb4456d1e3 Add spawn-rx 2024-11-26 16:52:22 +01:00
Anaïs Betts
780b92274d Make tailwind config work on Windows 2024-11-26 16:51:50 +01:00
ashwin-ant
b825784b8f Merge pull request #75 from modelcontextprotocol/readmeupdate
clarify readme
2024-11-25 14:01:07 -08:00
Ashwin Bhat
52c7e98055 clarify readme 2024-11-25 17:00:04 -05:00
ashwin-ant
4862aa7c1d Merge pull request #74 from simonw/patch-1
Skip the dist/index.js bit
2024-11-25 13:58:02 -08:00
Simon Willison
561ea91504 Skip the dist/index.js bit
The `dist/index.js` bit is confusing. Running without that gives you a working web UI.
2024-11-25 10:26:35 -08:00
Ashwin Bhat
7c2be8d139 bump version to 0.2.2 2024-11-25 10:35:26 -05:00
ashwin-ant
97d469911e Merge pull request #73 from modelcontextprotocol/ashwin/darkmodetoggle
make theme selectable in UI, store setting in localstorage
2024-11-25 06:24:59 -08:00
Ashwin Bhat
11b891c6ca make theme selectable in UI 2024-11-25 09:23:26 -05:00
Justin Spahr-Summers
5139e723a4 Bump package versions 2024-11-25 07:20:05 -06:00
Justin Spahr-Summers
fc5b79c9a6 Merge pull request #72 from modelcontextprotocol/ashwin/darkmodetool
more dark mode fixes
2024-11-24 16:37:52 -06:00
Ashwin Bhat
47a87e1884 more dark mode fixes 2024-11-24 14:26:39 -08:00
Justin Spahr-Summers
d567ff37e8 Merge pull request #70 from modelcontextprotocol/ashwin/errorlog
show server stderr in inspector UI
2024-11-21 10:43:03 +00:00
Justin Spahr-Summers
2e4eedc6ef Add publish workflow 2024-11-21 10:41:25 +00:00
Justin Spahr-Summers
56ec9befd9 Add publish-all script 2024-11-21 10:40:10 +00:00
Justin Spahr-Summers
360d090ac9 Fix inconsistent capitalization 2024-11-21 10:31:01 +00:00
Justin Spahr-Summers
fda05836cb Merge branch 'main' into ashwin/errorlog 2024-11-21 10:30:29 +00:00
Justin Spahr-Summers
dac692e638 Fix type errors in server 2024-11-21 10:29:36 +00:00
Justin Spahr-Summers
7e4b276f7f Bump versions 2024-11-21 10:29:28 +00:00
Justin Spahr-Summers
668448b047 Merge pull request #71 from modelcontextprotocol/ashwin/tabs
remove unused tabs
2024-11-21 10:22:54 +00:00
Ashwin Bhat
4352c93660 remove unused tabs 2024-11-20 19:06:26 -08:00
Ashwin Bhat
22bf78720b show server stderr in inspector UI 2024-11-20 18:13:54 -08:00
Ashwin Bhat
9196c1ddaf update sdk 2024-11-20 18:09:16 -08:00
ashwin-ant
dfc10488b2 Merge pull request #69 from modelcontextprotocol/ashwin/darkmode
dark mode fixes
2024-11-20 14:10:57 -08:00
Ashwin Bhat
78182eab10 dark mode fixes 2024-11-20 11:25:06 -08:00
Justin Spahr-Summers
7dbaee47a2 Merge pull request #68 from modelcontextprotocol/justin/contributing
Add CONTRIBUTING.md
2024-11-20 16:12:06 +00:00
Justin Spahr-Summers
03193a9dc4 Merge pull request #67 from modelcontextprotocol/justin/update-readme
Update README instructions
2024-11-20 16:11:37 +00:00
Justin Spahr-Summers
60dea9a868 Clarify testing and submission steps 2024-11-20 16:02:21 +00:00
Justin Spahr-Summers
1341d73775 Fix issues link 2024-11-20 16:00:03 +00:00
Justin Spahr-Summers
f73770b143 Merge pull request #66 from modelcontextprotocol/justin/fix-homepage
Fix packages' homepage URL
2024-11-20 15:59:29 +00:00
ashwin-ant
33ab8dbd97 Merge pull request #65 from modelcontextprotocol/ashwin/startup
improve inspector startup message
2024-11-20 07:55:29 -08:00
Justin Spahr-Summers
2d5c866f82 Add CONTRIBUTING.md 2024-11-20 15:26:05 +00:00
Justin Spahr-Summers
232d5ffcf6 Update README instructions 2024-11-20 14:48:02 +00:00
Justin Spahr-Summers
4f930a61ab Fix packages' homepage URL 2024-11-20 13:53:33 +00:00
Ashwin Bhat
f684d2e891 improve inspector startup message 2024-11-19 16:58:33 -08:00
ashwin-ant
676de45bab Merge pull request #64 from modelcontextprotocol/ashwin/spaces
fix arg passing in cli
2024-11-19 15:46:17 -08:00
Ashwin Bhat
9a560e3f06 handle quotes better 2024-11-19 15:42:53 -08:00
ashwin-ant
64d2fea0f1 Merge pull request #62 from modelcontextprotocol/ashwin/defaults
add back default command and args
2024-11-19 14:50:41 -08:00
Ashwin Bhat
1843562dce bump version 2024-11-19 14:22:00 -08:00
ashwin-ant
31630b2870 Merge pull request #63 from modelcontextprotocol/ashwin/favicon
switch favicon
2024-11-19 14:21:42 -08:00
Ashwin Bhat
7c04df5e2e fix arg passing in cli 2024-11-19 14:19:59 -08:00
Ashwin Bhat
f8e26479d9 switch favicon 2024-11-19 14:13:19 -08:00
Ashwin Bhat
0c6b1ad1d2 add back default command and args 2024-11-19 13:30:04 -08:00
ashwin-ant
b97acb9b76 Merge pull request #61 from modelcontextprotocol/ashwin/npx
make inspector runnable via npx
2024-11-19 13:27:57 -08:00
Ashwin Bhat
1b06e50203 use published versions of subpackages 2024-11-19 13:19:21 -08:00
Ashwin Bhat
83abe4e00f command 2024-11-19 13:08:42 -08:00
Ashwin Bhat
75bc2d3f26 bump version 2024-11-19 11:51:01 -08:00
Ashwin Bhat
b127ba177a fix arg 2024-11-19 11:50:45 -08:00
Ashwin Bhat
ce350433f9 bump version 2024-11-19 11:42:32 -08:00
Ashwin Bhat
c1a56810fb pass in args 2024-11-19 11:42:16 -08:00
Ashwin Bhat
abff2486c1 deps 2024-11-19 11:42:16 -08:00
David Soria Parra
b096f3f991 Update SECURITY.md 2024-11-19 16:49:04 +00:00
David Soria Parra
e97ec8dbc7 Merge pull request #60 from modelcontextprotocol/dsp-ant-patch-1
Update SECURITY.md
2024-11-19 12:16:05 +00:00
David Soria Parra
05c22f6367 Update SECURITY.md 2024-11-19 12:15:55 +00:00
David Soria Parra
3b3a7e6015 Create SECURITY.md 2024-11-19 12:13:49 +00:00
David Soria Parra
8986c32bb4 Update LICENSE 2024-11-18 22:24:35 +00:00
David Soria Parra
69b25300da Merge pull request #59 from modelcontextprotocol/davidsp/coc
Add code of conduct
2024-11-18 16:02:29 +00:00
David Soria Parra
6e72c7583e Add code of conduct 2024-11-18 15:32:16 +00:00
Justin Spahr-Summers
d1c9acffc2 Merge pull request #58 from modelcontextprotocol/justin/upgrade-sdk
Upgrade to SDK 0.5.0, add default request timeout
2024-11-16 16:02:54 +00:00
Justin Spahr-Summers
d91d7e8ae0 Add default request timeout 2024-11-16 15:54:08 +00:00
Justin Spahr-Summers
6c27f5a263 Upgrade to SDK 0.5.0 2024-11-16 15:25:02 +00:00
Justin Spahr-Summers
2bf84a3ef3 Merge pull request #57 from modelcontextprotocol/justin/fix-env-vars
Fix typing environment variable keys
2024-11-14 09:54:19 +00:00
Justin Spahr-Summers
8aad8b4aac Fix typing environment variable keys 2024-11-13 21:00:20 +00:00
Justin Spahr-Summers
06267d28f4 Merge pull request #52 from modelcontextprotocol/justin/tab-specific-errors
Separate error states per tab, clear errors when clicking around
2024-11-12 15:44:15 +00:00
Justin Spahr-Summers
c1c8fc2f42 Use clearError in makeRequest 2024-11-12 15:42:23 +00:00
Justin Spahr-Summers
7a350785fe Merge branch 'main' into justin/tab-specific-errors 2024-11-12 15:40:57 +00:00
Justin Spahr-Summers
41f8ec0868 Merge pull request #53 from modelcontextprotocol/justin/server-connection-errors
Fix server dying when an error occurs
2024-11-12 15:31:36 +00:00
Justin Spahr-Summers
09d66ab704 Merge pull request #55 from modelcontextprotocol/justin/multiline-tool-inputs
Support multiline tool inputs
2024-11-12 15:31:30 +00:00
Justin Spahr-Summers
e4faa19acb Merge pull request #54 from modelcontextprotocol/justin/redo-sidebars
Revamp UI + panes
2024-11-12 15:31:21 +00:00
Justin Spahr-Summers
f7385dd961 Support multiline tool inputs
Resolves #39.
2024-11-12 14:59:36 +00:00
Justin Spahr-Summers
54012aca6a Make bottom pane resizable 2024-11-12 14:43:56 +00:00
Justin Spahr-Summers
0cf344bb6a Move "history and notifications" pane to bottom 2024-11-12 14:35:53 +00:00
Justin Spahr-Summers
a507bafc3e Remove big header in main pane 2024-11-12 14:28:56 +00:00
Justin Spahr-Summers
93b1ec4d61 Move connection settings UI into left sidebar 2024-11-12 14:27:02 +00:00
Justin Spahr-Summers
bf2ddc9b7b Fix server dying when an error occurs
Resolves #34.
2024-11-12 14:02:23 +00:00
Justin Spahr-Summers
da2ac8d423 Clear error states when clicking around 2024-11-12 13:51:00 +00:00
Justin Spahr-Summers
3bae26723a Use toasts for errors unassociated with a tab 2024-11-12 13:50:59 +00:00
Justin Spahr-Summers
9d0c643926 Install react-toastify 2024-11-12 13:50:59 +00:00
Justin Spahr-Summers
0716adafc6 Remove unused RequestsTab 2024-11-12 13:50:59 +00:00
Justin Spahr-Summers
733d2a6e6e Separate error states per tab
Resolves #40.
2024-11-12 13:50:59 +00:00
Justin Spahr-Summers
ab9c130610 Merge pull request #50 from modelcontextprotocol/justin/fix-dark-mode
Marginally better dark mode support
2024-11-12 13:46:24 +00:00
Justin Spahr-Summers
3e46011614 Marginally better dark mode support 2024-11-12 12:34:10 +00:00
Justin Spahr-Summers
584c1076a4 Merge pull request #49 from modelcontextprotocol/justin/fix-paths
Remove example paths now that SDK pathing works better
2024-11-11 19:45:13 +00:00
Justin Spahr-Summers
de9ee3956e Remove example paths now that SDK pathing works better 2024-11-11 17:45:07 +00:00
Justin Spahr-Summers
01fb48e6ef Merge pull request #48 from modelcontextprotocol/jspahrsummers-patch-1
Update setup instructions in README
2024-11-11 16:36:59 +00:00
Justin Spahr-Summers
5ceaa48cf3 Update setup instructions in README 2024-11-11 16:36:42 +00:00
Justin Spahr-Summers
6fdb2903a4 Merge pull request #47 from modelcontextprotocol/justin/pull-from-npm
Pull SDK from npm
2024-11-11 16:33:22 +00:00
Justin Spahr-Summers
b9d6695fbf Revert "Try restoring npm ci"
This reverts commit 3789ef984a.
2024-11-11 16:31:24 +00:00
Justin Spahr-Summers
3789ef984a Try restoring npm ci 2024-11-11 16:29:09 +00:00
Justin Spahr-Summers
e1aa419332 Pull SDK from npm 2024-11-11 16:28:51 +00:00
David Soria Parra
94e4e92618 Merge pull request #45 from modelcontextprotocol/davidsp/fix-capture 2024-11-11 12:33:27 +00:00
Justin Spahr-Summers
148e84388a Merge pull request #46 from modelcontextprotocol/justin/oss-prep
Add license and package metadata
2024-11-11 12:22:32 +00:00
Justin Spahr-Summers
6147640656 Remove client prepare script 2024-11-11 12:19:14 +00:00
Justin Spahr-Summers
1ddd2d7aaa Disallow publishing root package for now 2024-11-11 12:15:52 +00:00
Justin Spahr-Summers
94ddfed1ac Add concurrently dependency 2024-11-11 11:24:21 +00:00
Justin Spahr-Summers
448910d986 Make nested packages private, just install root package 2024-11-11 11:20:53 +00:00
Justin Spahr-Summers
3a9b08bd37 Pick non-conflicting default client port 2024-11-11 11:11:27 +00:00
Justin Spahr-Summers
bce3a7b8d6 Make root package installable/executable 2024-11-11 11:10:42 +00:00
Justin Spahr-Summers
196f2f801d Use serve to create npm package for the client 2024-11-11 11:04:42 +00:00
Justin Spahr-Summers
a0d8ec1e7e Make server executable on its own 2024-11-11 10:41:07 +00:00
Justin Spahr-Summers
6759c461f9 Add license and more package metadata 2024-11-11 10:37:09 +00:00
David Soria Parra
ea4484cc04 Fix bug that roots returned by roots/list are never updated
The roots state is capture at the initialization of the connectMcpServer.
This causes roots/list to always return empty roots despite updates
to the roots. We are now using a ref to reference it and avoid the closure
issue. roots/list now returns correctly the roots.
2024-11-10 23:45:38 +00:00
Justin Spahr-Summers
171b59bec6 Merge pull request #44 from modelcontextprotocol/justin/update-sdk
Update SDK; add UI for roots, resource templates, and env vars
2024-11-08 12:06:06 +00:00
Justin Spahr-Summers
2867173e7b Record 'roots list changed' notifications in history sidebar 2024-11-08 12:04:45 +00:00
Justin Spahr-Summers
57f0c49154 Remove redundant useCallbacks 2024-11-08 12:00:46 +00:00
Justin Spahr-Summers
5337baa116 Fix type error 2024-11-07 15:35:55 +00:00
Justin Spahr-Summers
afefcb3fa5 Make env vars UI toggleable 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
76e2cf6fdc Add UI for viewing and configuring environment variables 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
193032533b Support structured tool results 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
f3406ca43d Basic support for roots 2024-11-07 14:27:46 +00:00
Justin Spahr-Summers
645f2e942e Add support for listing and filling resource templates 2024-11-07 14:02:53 +00:00
Justin Spahr-Summers
d80214d0a2 Update SDK to 0.3.2 2024-11-07 13:46:21 +00:00
David Soria Parra
97b67ca1ae Merge pull request #43 from modelcontextprotocol/davidsp/prettier
run prettier
2024-11-05 10:40:07 +00:00
David Soria Parra
216c8a15d5 run prettier 2024-11-04 21:29:59 +00:00
44 changed files with 3582 additions and 1255 deletions

View File

@@ -4,6 +4,8 @@ on:
- main
pull_request:
release:
types: [published]
jobs:
build:
@@ -17,21 +19,34 @@ jobs:
node-version: 18
cache: npm
- uses: actions/checkout@v4
with:
repository: modelcontextprotocol/typescript-sdk
ssh-key: ${{ secrets.TYPESCRIPT_SDK_KEY }}
path: packages/@modelcontextprotocol/sdk
- run: npm ci
working-directory: packages/@modelcontextprotocol/sdk
- run: npm pack
working-directory: packages/@modelcontextprotocol/sdk
- run: npm install --save packages/@modelcontextprotocol/sdk/modelcontextprotocol-sdk-*.tgz
# Working around https://github.com/npm/cli/issues/4828
# - run: npm ci
- run: npm install --no-package-lock
- run: npm run build
publish:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: release
needs: build
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
registry-url: "https://registry.npmjs.org"
# Working around https://github.com/npm/cli/issues/4828
# - run: npm ci
- run: npm install --no-package-lock
# TODO: Add --provenance once the repo is public
- run: npm run publish-all
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

1
.npmrc
View File

@@ -1 +1,2 @@
registry="https://registry.npmjs.org/"
@modelcontextprotocol:registry="https://registry.npmjs.org/"

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
mcp-coc@anthropic.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

37
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,37 @@
# Contributing to Model Context Protocol Inspector
Thanks for your interest in contributing! This guide explains how to get involved.
## Getting Started
1. Fork the repository and clone it locally
2. Install dependencies with `npm install`
3. Run `npm run dev` to start both client and server in development mode
4. Use the web UI at http://localhost:5173 to interact with the inspector
## Development Process & Pull Requests
1. Create a new branch for your changes
2. Make your changes following existing code style and conventions
3. Test changes locally
4. Update documentation as needed
5. Use clear commit messages explaining your changes
6. Verify all changes work as expected
7. Submit a pull request
8. PRs will be reviewed by maintainers
## Code of Conduct
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
## Security
If you find a security vulnerability, please refer to our [Security Policy](SECURITY.md) for reporting instructions.
## Questions?
Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics.
## License
By contributing, you agree that your contributions will be licensed under the MIT license.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Anthropic, PBC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -2,30 +2,47 @@
The MCP inspector is a developer tool for testing and debugging MCP servers.
## Getting started
![MCP Inspector Screenshot](mcp-inspector.png)
This repository depends on the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk/). Until these repositories are made public and published to npm, the SDK has to be preinstalled manually:
## Running the Inspector
1. Download the [latest release of the SDK](https://github.com/modelcontextprotocol/typescript-sdk/releases) (the file named something like `modelcontextprotocol-sdk-0.1.0.tgz`). You don't need to extract it.
2. From within your checkout of _this_ repository, run `npm install --save path/to/sdk.tgz`. This will overwrite the expected location for the SDK to allow you to proceed.
### From an MCP server repository
Then, you should be able to install the rest of the dependencies normally:
To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`:
```sh
npm install
```bash
npx @modelcontextprotocol/inspector build/index.js
```
You can run it in dev mode via:
You can also pass arguments along which will get passed as arguments to your MCP server:
```
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
```
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:
```bash
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js
```
### From this repository
If you're working on the inspector itself:
Development mode:
```bash
npm run dev
```
This will start both the client and server.
To run in production mode:
Production mode:
```bash
npm run build
npm start
```
## License
This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
Thank you for helping us keep the inspector secure.
## Reporting Security Issues
This project is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
The security of our systems and user data is Anthropics top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
## Vulnerability Disclosure Program
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).

88
bin/cli.js Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env node
import { resolve, dirname } from "path";
import { spawnPromise } from "spawn-rx";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
// Get command line arguments
const [, , command, ...mcpServerArgs] = process.argv;
const inspectorServerPath = resolve(
__dirname,
"..",
"server",
"build",
"index.js",
);
// Path to the client entry point
const inspectorClientPath = resolve(
__dirname,
"..",
"client",
"bin",
"cli.js",
);
const CLIENT_PORT = process.env.CLIENT_PORT ?? "5173";
const SERVER_PORT = process.env.SERVER_PORT ?? "3000";
console.log("Starting MCP inspector...");
const abort = new AbortController();
let cancelled = false;
process.on("SIGINT", () => {
cancelled = true;
abort.abort();
});
const server = spawnPromise(
"node",
[
inspectorServerPath,
...(command ? [`--env`, command] : []),
...(mcpServerArgs ? ["--args", mcpServerArgs.join(" ")] : []),
],
{
env: { ...process.env, PORT: SERVER_PORT },
signal: abort.signal,
echoOutput: true,
},
);
const client = spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
// Make sure our server/client didn't immediately fail
await Promise.any([server, client, delay(2 * 1000)]);
const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`;
console.log(
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} 🚀`,
);
try {
await Promise.any([server, client]);
} catch (e) {
if (!cancelled || process.env.DEBUG) throw e;
}
return 0;
}
main()
.then((_) => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});

16
client/bin/cli.js Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
import http from "http";
const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = join(__dirname, "../dist");
const server = http.createServer((request, response) => {
return handler(request, response, { public: distPath });
});
const port = process.env.PORT || 5173;
server.listen(port, () => {});

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/mcp.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP Inspector</title>
</head>

View File

@@ -1,8 +1,19 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"name": "@modelcontextprotocol/inspector-client",
"version": "0.2.4",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector-client": "./bin/cli.js"
},
"files": [
"bin",
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
@@ -10,7 +21,7 @@
"preview": "vite preview"
},
"dependencies": {
"@modelcontextprotocol/sdk": "*",
"@modelcontextprotocol/sdk": "^1.0.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
@@ -21,6 +32,8 @@
"lucide-react": "^0.447.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-toastify": "^10.0.6",
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@@ -30,6 +43,7 @@
"@types/node": "^22.7.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/serve-handler": "^6.1.4",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.11.1",

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};

12
client/public/mcp.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_19_13)">
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706V16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087V118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822" stroke="black" stroke-width="12" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_19_13">
<rect width="180" height="180" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,86 +1,104 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
CallToolResultSchema,
ClientNotification,
ClientRequest,
CompatibilityCallToolResult,
CompatibilityCallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResult,
EmptyResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema,
ProgressNotificationSchema,
ReadResourceResultSchema,
Request,
Resource,
ResourceTemplate,
Result,
Root,
ServerNotification,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
Notification,
StdErrNotification,
StdErrNotificationSchema,
} from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Bell,
Files,
FolderTree,
Hammer,
Hash,
MessageSquare,
Play,
Send,
Terminal,
} from "lucide-react";
import { AnyZodObject } from "zod";
import { toast } from "react-toastify";
import { ZodType } from "zod";
import "./App.css";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
const params = new URLSearchParams(window.location.search);
const PROXY_PORT = params.get("proxyPort") ?? "3000";
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
const App = () => {
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
>([]);
const [resourceContent, setResourceContent] = useState<string>("");
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [toolResult, setToolResult] =
useState<CompatibilityCallToolResult | null>(null);
const [errors, setErrors] = useState<Record<string, string | null>>({
resources: null,
prompts: null,
tools: null,
});
const [command, setCommand] = useState<string>(() => {
return (
localStorage.getItem("lastCommand") ||
"/Users/ashwin/.nvm/versions/node/v18.20.4/bin/node"
);
return localStorage.getItem("lastCommand") || "mcp-server-everything";
});
const [args, setArgs] = useState<string>(() => {
return (
localStorage.getItem("lastArgs") ||
"/Users/ashwin/code/mcp/example-servers/build/everything/stdio.js"
);
return localStorage.getItem("lastArgs") || "";
});
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
const [requestHistory, setRequestHistory] = useState<
{ request: string; response: string }[]
{ request: string; response?: string }[]
>([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [stdErrNotifications, setStdErrNotifications] = useState<
StdErrNotification[]
>([]);
const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({});
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
@@ -91,6 +109,7 @@ const App = () => {
>
>([]);
const nextRequestId = useRef(0);
const rootsRef = useRef<Root[]>([]);
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => {
@@ -116,11 +135,57 @@ const App = () => {
const [nextResourceCursor, setNextResourceCursor] = useState<
string | undefined
>();
const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
string | undefined
>();
const [nextPromptCursor, setNextPromptCursor] = useState<
string | undefined
>();
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0);
const [historyPaneHeight, setHistoryPaneHeight] = useState<number>(300);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = historyPaneHeight;
document.body.style.userSelect = "none";
},
[historyPaneHeight],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHistoryPaneHeight(newHeight);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
useEffect(() => {
localStorage.setItem("lastCommand", command);
@@ -130,27 +195,96 @@ const App = () => {
localStorage.setItem("lastArgs", args);
}, [args]);
const pushHistory = (request: object, response: object) => {
useEffect(() => {
fetch(`${PROXY_SERVER_URL}/config`)
.then((response) => response.json())
.then((data) => {
setEnv(data.defaultEnvironment);
if (data.defaultCommand) {
setCommand(data.defaultCommand);
}
if (data.defaultArgs) {
setArgs(data.defaultArgs);
}
})
.catch((error) =>
console.error("Error fetching default environment:", error),
);
}, []);
useEffect(() => {
rootsRef.current = roots;
}, [roots]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{ request: JSON.stringify(request), response: JSON.stringify(response) },
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const makeRequest = async <T extends AnyZodObject>(
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};
const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const response = await mcpClient.request(request, schema);
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, DEFAULT_REQUEST_TIMEOUT_MSEC);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: abortController.signal,
});
} finally {
clearTimeout(timeoutId);
}
pushHistory(request, response);
if (tabKey !== undefined) {
clearError(tabKey);
}
return response;
} catch (e: unknown) {
setError((e as Error).message);
const errorString = (e as Error).message ?? String(e);
if (tabKey === undefined) {
toast.error(errorString);
} else {
setErrors((prev) => ({
...prev,
[tabKey]: errorString,
}));
}
throw e;
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
toast.error((e as Error).message ?? String(e));
throw e;
}
};
@@ -162,11 +296,29 @@ const App = () => {
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
},
ListResourcesResultSchema,
"resources",
);
setResources(resources.concat(response.resources ?? []));
setNextResourceCursor(response.nextCursor);
};
const listResourceTemplates = async () => {
const response = await makeRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
? { cursor: nextResourceTemplateCursor }
: {},
},
ListResourceTemplatesResultSchema,
"resources",
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
);
setNextResourceTemplateCursor(response.nextCursor);
};
const readResource = async (uri: string) => {
const response = await makeRequest(
{
@@ -174,6 +326,7 @@ const App = () => {
params: { uri },
},
ReadResourceResultSchema,
"resources",
);
setResourceContent(JSON.stringify(response, null, 2));
};
@@ -185,6 +338,7 @@ const App = () => {
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
},
ListPromptsResultSchema,
"prompts",
);
setPrompts(response.prompts);
setNextPromptCursor(response.nextCursor);
@@ -197,6 +351,7 @@ const App = () => {
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
@@ -208,6 +363,7 @@ const App = () => {
params: nextToolCursor ? { cursor: nextToolCursor } : {},
},
ListToolsResultSchema,
"tools",
);
setTools(response.tools);
setNextToolCursor(response.nextCursor);
@@ -225,31 +381,46 @@ const App = () => {
},
},
},
CallToolResultSchema,
CompatibilityCallToolResultSchema,
"tools",
);
setToolResult(JSON.stringify(response.toolResult, null, 2));
setToolResult(response);
};
const handleRootsChange = async () => {
await sendNotification({ method: "notifications/roots/list_changed" });
};
const connectMcpServer = async () => {
try {
const client = new Client({
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: "0.0.1",
});
},
{
capabilities: {
// Support all client capabilities since we're an inspector tool
sampling: {},
roots: {
listChanged: true,
},
},
},
);
const backendUrl = new URL("http://localhost:3000/sse");
const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`);
backendUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
backendUrl.searchParams.append("command", command);
backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env));
} else {
backendUrl.searchParams.append("url", url);
backendUrl.searchParams.append("url", sseUrl);
}
const clientTransport = new SSEClientTransport(backendUrl);
await client.connect(clientTransport);
client.setNotificationHandler(
ProgressNotificationSchema,
(notification) => {
@@ -260,6 +431,18 @@ const App = () => {
},
);
client.setNotificationHandler(
StdErrNotificationSchema,
(notification) => {
setStdErrNotifications((prevErrorNotifications) => [
...prevErrorNotifications,
notification,
]);
},
);
await client.connect(clientTransport);
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
return new Promise<CreateMessageResult>((resolve, reject) => {
setPendingSampleRequests((prev) => [
@@ -269,6 +452,10 @@ const App = () => {
});
});
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots: rootsRef.current };
});
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
@@ -278,55 +465,24 @@ const App = () => {
};
return (
<div className="flex h-screen bg-gray-100">
<Sidebar connectionStatus={connectionStatus} />
<div className="flex h-screen bg-background">
<Sidebar
connectionStatus={connectionStatus}
transportType={transportType}
setTransportType={setTransportType}
command={command}
setCommand={setCommand}
args={args}
setArgs={setArgs}
sseUrl={sseUrl}
setSseUrl={setSseUrl}
env={env}
setEnv={setEnv}
onConnect={connectMcpServer}
stdErrNotifications={stdErrNotifications}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<h1 className="text-2xl font-bold p-4">MCP Inspector</h1>
<div className="flex-1 overflow-auto flex">
<div className="flex-1">
<div className="p-4 bg-white shadow-md m-4 rounded-md">
<h2 className="text-lg font-semibold mb-2">Connect MCP Server</h2>
<div className="flex space-x-2 mb-2">
<Select
value={transportType}
onValueChange={(value: "stdio" | "sse") =>
setTransportType(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">STDIO</SelectItem>
<SelectItem value="sse">SSE</SelectItem>
</SelectContent>
</Select>
{transportType === "stdio" ? (
<>
<Input
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
/>
<Input
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</>
) : (
<Input
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
)}
<Button onClick={connectMcpServer}>
<Play className="w-4 h-4 mr-2" />
Connect
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4">
<TabsList className="mb-4 p-0">
@@ -338,18 +494,10 @@ const App = () => {
<MessageSquare className="w-4 h-4 mr-2" />
Prompts
</TabsTrigger>
<TabsTrigger value="requests" disabled>
<Send className="w-4 h-4 mr-2" />
Requests
</TabsTrigger>
<TabsTrigger value="tools">
<Hammer className="w-4 h-4 mr-2" />
Tools
</TabsTrigger>
<TabsTrigger value="console" disabled>
<Terminal className="w-4 h-4 mr-2" />
Console
</TabsTrigger>
<TabsTrigger value="ping">
<Bell className="w-4 h-4 mr-2" />
Ping
@@ -363,42 +511,76 @@ const App = () => {
</span>
)}
</TabsTrigger>
<TabsTrigger value="roots">
<FolderTree className="w-4 h-4 mr-2" />
Roots
</TabsTrigger>
</TabsList>
<div className="w-full">
<ResourcesTab
resources={resources}
listResources={listResources}
readResource={readResource}
resourceTemplates={resourceTemplates}
listResources={() => {
clearError("resources");
listResources();
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={setSelectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
error={error}
nextTemplateCursor={nextResourceTemplateCursor}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={listPrompts}
getPrompt={getPrompt}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={setSelectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={error}
error={errors.prompts}
/>
<RequestsTab />
<ToolsTab
tools={tools}
listTools={listTools}
callTool={callTool}
listTools={() => {
clearError("tools");
listTools();
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult("");
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={error}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
@@ -416,6 +598,11 @@ const App = () => {
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</div>
</Tabs>
) : (
@@ -426,13 +613,27 @@ const App = () => {
</div>
)}
</div>
<div
className="relative border-t border-border"
style={{
height: `${historyPaneHeight}px`,
}}
>
<div
className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50"
onMouseDown={handleDragStart}
>
<div className="w-8 h-1 rounded-full bg-border" />
</div>
</div>
<div className="h-full overflow-auto">
<HistoryAndNotifications
requestHistory={requestHistory}
serverNotifications={notifications}
/>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -6,7 +6,7 @@ const HistoryAndNotifications = ({
requestHistory,
serverNotifications,
}: {
requestHistory: Array<{ request: string; response: string | null }>;
requestHistory: Array<{ request: string; response?: string }>;
serverNotifications: ServerNotification[];
}) => {
const [expandedRequests, setExpandedRequests] = useState<{
@@ -29,8 +29,8 @@ const HistoryAndNotifications = ({
};
return (
<div className="w-64 bg-white shadow-md p-4 overflow-hidden flex flex-col h-full">
<div className="flex-1 overflow-y-auto mb-4 border-b pb-4">
<div className="bg-card overflow-hidden flex h-full">
<div className="flex-1 overflow-y-auto p-4 border-r">
<h2 className="text-lg font-semibold mb-4">History</h2>
{requestHistory.length === 0 ? (
<p className="text-sm text-gray-500 italic">No history yet</p>
@@ -42,7 +42,7 @@ const HistoryAndNotifications = ({
.map((request, index) => (
<li
key={index}
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
className="text-sm text-foreground bg-secondary p-2 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
@@ -74,7 +74,7 @@ const HistoryAndNotifications = ({
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-blue-50 p-2 rounded">
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
{JSON.stringify(JSON.parse(request.request), null, 2)}
</pre>
</div>
@@ -91,7 +91,7 @@ const HistoryAndNotifications = ({
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-green-50 p-2 rounded">
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
{JSON.stringify(
JSON.parse(request.response),
null,
@@ -107,7 +107,7 @@ const HistoryAndNotifications = ({
</ul>
)}
</div>
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto p-4">
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
{serverNotifications.length === 0 ? (
<p className="text-sm text-gray-500 italic">No notifications yet</p>
@@ -119,7 +119,7 @@ const HistoryAndNotifications = ({
.map((notification, index) => (
<li
key={index}
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
className="text-sm text-foreground bg-secondary p-2 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
@@ -146,7 +146,7 @@ const HistoryAndNotifications = ({
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-purple-50 p-2 rounded">
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
{JSON.stringify(notification, null, 2)}
</pre>
</div>

View File

@@ -19,9 +19,9 @@ const ListPane = <T extends object>({
buttonText,
isButtonDisabled,
}: ListPaneProps<T>) => (
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<h3 className="font-semibold">{title}</h3>
<div className="bg-card rounded-lg shadow">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="font-semibold dark:text-white">{title}</h3>
</div>
<div className="p-4">
<Button
@@ -36,7 +36,7 @@ const ListPane = <T extends object>({
{items.map((item, index) => (
<div
key={index}
className="flex items-center p-2 rounded hover:bg-gray-50 cursor-pointer"
className="flex items-center p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
onClick={() => setSelectedItem(item)}
>
{renderItem(item)}

View File

@@ -70,7 +70,7 @@ const PromptsTab = ({
isButtonDisabled={!nextCursor && prompts.length > 0}
/>
<div className="bg-white rounded-lg shadow">
<div className="bg-card rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<h3 className="font-semibold">
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}

View File

@@ -1,33 +0,0 @@
import { TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Send } from "lucide-react";
const RequestsTab = () => (
<TabsContent value="requests" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4">
<div className="flex space-x-2">
<Input placeholder="Method name" />
<Button>
<Send className="w-4 h-4 mr-2" />
Send
</Button>
</div>
<Textarea
placeholder="Request parameters (JSON)"
className="h-64 font-mono"
/>
</div>
<div>
<div className="bg-gray-50 p-4 rounded-lg h-96 font-mono text-sm overflow-auto">
<div className="text-gray-500 mb-2">Response:</div>
{/* Response content would go here */}
</div>
</div>
</div>
</TabsContent>
);
export default RequestsTab;

View File

@@ -1,36 +1,77 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { ListResourcesResult, Resource } from "@modelcontextprotocol/sdk/types.js";
import {
ListResourcesResult,
Resource,
ResourceTemplate,
ListResourceTemplatesResult,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
import ListPane from "./ListPane";
import { useState } from "react";
const ResourcesTab = ({
resources,
resourceTemplates,
listResources,
listResourceTemplates,
readResource,
selectedResource,
setSelectedResource,
resourceContent,
nextCursor,
nextTemplateCursor,
error,
}: {
resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void;
listResourceTemplates: () => void;
readResource: (uri: string) => void;
selectedResource: Resource | null;
setSelectedResource: (resource: Resource) => void;
setSelectedResource: (resource: Resource | null) => void;
resourceContent: string;
nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null;
}) => (
<TabsContent value="resources" className="grid grid-cols-2 gap-4">
}) => {
const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null);
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
{},
);
const fillTemplate = (
template: string,
values: Record<string, string>,
): string => {
return template.replace(
/{([^}]+)}/g,
(_, key) => values[key] || `{${key}}`,
);
};
const handleReadTemplateResource = () => {
if (selectedTemplate) {
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
readResource(uri);
setSelectedTemplate(null);
// We don't have the full Resource object here, so we create a partial one
setSelectedResource({ uri, name: uri } as Resource);
}
};
return (
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
<ListPane
items={resources}
listItems={listResources}
setSelectedItem={(resource) => {
setSelectedResource(resource);
readResource(resource.uri);
setSelectedTemplate(null);
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
@@ -46,10 +87,41 @@ const ResourcesTab = ({
isButtonDisabled={!nextCursor && resources.length > 0}
/>
<div className="bg-white rounded-lg shadow">
<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
setSelectedItem={(template) => {
setSelectedTemplate(template);
setSelectedResource(null);
setTemplateValues({});
}}
renderItem={(template) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={template.uriTemplate}>
{template.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resource Templates"
buttonText={
nextTemplateCursor ? "List More Templates" : "List Templates"
}
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
/>
<div className="bg-card rounded-lg shadow">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 className="font-semibold truncate" title={selectedResource?.name}>
{selectedResource ? selectedResource.name : "Select a resource"}
<h3
className="font-semibold truncate"
title={selectedResource?.name || selectedTemplate?.name}
>
{selectedResource
? selectedResource.name
: selectedTemplate
? selectedTemplate.name
: "Select a resource or template"}
</h3>
{selectedResource && (
<Button
@@ -70,13 +142,51 @@ const ResourcesTab = ({
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedResource ? (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
<pre className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
{resourceContent}
</pre>
) : selectedTemplate ? (
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedTemplate.description}
</p>
{selectedTemplate.uriTemplate
.match(/{([^}]+)}/g)
?.map((param) => {
const key = param.slice(1, -1);
return (
<div key={key}>
<label
htmlFor={key}
className="block text-sm font-medium text-gray-700"
>
{key}
</label>
<Input
id={key}
value={templateValues[key] || ""}
onChange={(e) =>
setTemplateValues({
...templateValues,
[key]: e.target.value,
})
}
className="mt-1"
/>
</div>
);
})}
<Button
onClick={handleReadTemplateResource}
disabled={Object.keys(templateValues).length === 0}
>
Read Resource
</Button>
</div>
) : (
<Alert>
<AlertDescription>
Select a resource from the list to view its contents
Select a resource or template from the list to view its contents
</AlertDescription>
</Alert>
)}
@@ -84,5 +194,6 @@ const ResourcesTab = ({
</div>
</TabsContent>
);
};
export default ResourcesTab;

View File

@@ -0,0 +1,77 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { Root } from "@modelcontextprotocol/sdk/types.js";
import { Plus, Minus, Save } from "lucide-react";
const RootsTab = ({
roots,
setRoots,
onRootsChange,
}: {
roots: Root[];
setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
onRootsChange: () => void;
}) => {
const addRoot = () => {
setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
};
const removeRoot = (index: number) => {
setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
};
const updateRoot = (index: number, field: keyof Root, value: string) => {
setRoots((currentRoots) =>
currentRoots.map((root, i) =>
i === index ? { ...root, [field]: value } : root,
),
);
};
const handleSave = () => {
onRootsChange();
};
return (
<TabsContent value="roots" className="space-y-4">
<Alert>
<AlertDescription>
Configure the root directories that the server can access
</AlertDescription>
</Alert>
{roots.map((root, index) => (
<div key={index} className="flex gap-2 items-center">
<Input
placeholder="file:// URI"
value={root.uri}
onChange={(e) => updateRoot(index, "uri", e.target.value)}
className="flex-1"
/>
<Button
variant="destructive"
size="sm"
onClick={() => removeRoot(index)}
>
<Minus className="h-4 w-4" />
</Button>
</div>
))}
<div className="flex gap-2">
<Button variant="outline" onClick={addRoot}>
<Plus className="h-4 w-4 mr-2" />
Add Root
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
Save Changes
</Button>
</div>
</TabsContent>
);
};
export default RootsTab;

View File

@@ -1,15 +1,185 @@
import { Menu, Settings } from "lucide-react";
import { useState } from "react";
import { Play, ChevronDown, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { StdErrNotification } from "@/lib/notificationTypes";
const Sidebar = ({ connectionStatus }: { connectionStatus: string }) => (
<div className="w-64 bg-white border-r border-gray-200">
<div className="flex items-center p-4 border-b border-gray-200">
<Menu className="w-6 h-6 text-gray-500" />
<h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1>
import useTheme from "../lib/useTheme";
import { version } from "../../../package.json";
interface SidebarProps {
connectionStatus: "disconnected" | "connected" | "error";
transportType: "stdio" | "sse";
setTransportType: (type: "stdio" | "sse") => void;
command: string;
setCommand: (command: string) => void;
args: string;
setArgs: (args: string) => void;
sseUrl: string;
setSseUrl: (url: string) => void;
env: Record<string, string>;
setEnv: (env: Record<string, string>) => void;
onConnect: () => void;
stdErrNotifications: StdErrNotification[];
}
const Sidebar = ({
connectionStatus,
transportType,
setTransportType,
command,
setCommand,
args,
setArgs,
sseUrl,
setSseUrl,
env,
setEnv,
onConnect,
stdErrNotifications,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center">
<h1 className="ml-2 text-lg font-semibold">
MCP Inspector v{version}
</h1>
</div>
</div>
<div className="p-4">
<div className="flex items-center space-x-2 mb-4">
<div className="p-4 flex-1 overflow-auto">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Transport Type</label>
<Select
value={transportType}
onValueChange={(value: "stdio" | "sse") =>
setTransportType(value)
}
>
<SelectTrigger>
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">STDIO</SelectItem>
<SelectItem value="sse">SSE</SelectItem>
</SelectContent>
</Select>
</div>
{transportType === "stdio" ? (
<>
<div className="space-y-2">
<label className="text-sm font-medium">Command</label>
<Input
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Arguments</label>
<Input
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</div>
</>
) : (
<div className="space-y-2">
<label className="text-sm font-medium">URL</label>
<Input
placeholder="URL"
value={sseUrl}
onChange={(e) => setSseUrl(e.target.value)}
/>
</div>
)}
{transportType === "stdio" && (
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowEnvVars(!showEnvVars)}
className="flex items-center w-full"
>
{showEnvVars ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Environment Variables
</Button>
{showEnvVars && (
<div className="space-y-2">
{Object.entries(env).map(([key, value], idx) => (
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
<div className="space-y-1">
<Input
placeholder="Key"
value={key}
onChange={(e) => {
const newEnv = { ...env };
delete newEnv[key];
newEnv[e.target.value] = value;
setEnv(newEnv);
}}
/>
<Input
placeholder="Value"
value={value}
onChange={(e) => {
const newEnv = { ...env };
newEnv[key] = e.target.value;
setEnv(newEnv);
}}
/>
</div>
<Button
variant="destructive"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: removed, ...rest } = env;
setEnv(rest);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
onClick={() => {
const newEnv = { ...env };
newEnv[""] = "";
setEnv(newEnv);
}}
>
Add Environment Variable
</Button>
</div>
)}
</div>
)}
<div className="space-y-2">
<Button className="w-full" onClick={onConnect}>
<Play className="w-4 h-4 mr-2" />
Connect
</Button>
<div className="flex items-center justify-center space-x-2 mb-4">
<div
className={`w-2 h-2 rounded-full ${
connectionStatus === "connected"
@@ -27,13 +197,49 @@ const Sidebar = ({ connectionStatus }: { connectionStatus: string }) => (
: "Disconnected"}
</span>
</div>
<Button variant="outline" className="w-full justify-start">
<Settings className="w-4 h-4 mr-2" />
Connection Settings
</Button>
{stdErrNotifications.length > 0 && (
<>
<div className="mt-4 border-t border-gray-200 pt-4">
<h3 className="text-sm font-medium">
Error output from MCP server
</h3>
<div className="mt-2 max-h-80 overflow-y-auto">
{stdErrNotifications.map((notification, index) => (
<div
key={index}
className="text-sm text-red-500 font-mono py-2 border-b border-gray-200 last:border-b-0"
>
{notification.params.content}
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
</div>
<div className="p-4 border-t">
<div className="flex items-center space-x-2">
<Select
value={theme}
onValueChange={(value: string) =>
setTheme(value as "system" | "light" | "dark")
}
>
<SelectTrigger className="w-[120px]" id="theme-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
);
};
export default Sidebar;

View File

@@ -3,11 +3,18 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";
import { ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { Textarea } from "@/components/ui/textarea";
import {
ListToolsResult,
Tool,
CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
const ToolsTab = ({
tools,
listTools,
@@ -23,12 +30,79 @@ const ToolsTab = ({
callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void;
toolResult: string;
toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
const renderToolResult = () => {
if (!toolResult) return null;
if ("content" in toolResult) {
const parsedResult = CallToolResultSchema.safeParse(toolResult);
if (!parsedResult.success) {
return (
<>
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult, null, 2)}
</pre>
<h4 className="font-semibold mb-2">Errors:</h4>
{parsedResult.error.errors.map((error, idx) => (
<pre
key={idx}
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
>
{JSON.stringify(error, null, 2)}
</pre>
))}
</>
);
}
const structuredResult = parsedResult.data;
const isError = structuredResult.isError ?? false;
return (
<>
<h4 className="font-semibold mb-2">
Tool Result: {isError ? "Error" : "Success"}
</h4>
{structuredResult.content.map((item, index) => (
<div key={index} className="mb-2">
{item.type === "text" && (
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
{item.text}
</pre>
)}
{item.type === "image" && (
<img
src={`data:${item.mimeType};base64,${item.data}`}
alt="Tool result image"
className="max-w-full h-auto"
/>
)}
{item.type === "resource" && (
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)}
</pre>
)}
</div>
))}
</>
);
} else if ("toolResult" in toolResult) {
return (
<>
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
);
}
};
return (
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
<ListPane
@@ -48,7 +122,7 @@ const ToolsTab = ({
isButtonDisabled={!nextCursor && tools.length > 0}
/>
<div className="bg-white rounded-lg shadow">
<div className="bg-card rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<h3 className="font-semibold">
{selectedTool ? selectedTool.name : "Select a tool"}
@@ -75,6 +149,23 @@ const ToolsTab = ({
>
{key}
</Label>
{
/* @ts-expect-error value type is currently unknown */
value.type === "string" ? (
<Textarea
id={key}
name={key}
// @ts-expect-error value type is currently unknown
placeholder={value.description}
onChange={(e) =>
setParams({
...params,
[key]: e.target.value,
})
}
className="mt-1"
/>
) : (
<Input
// @ts-expect-error value type is currently unknown
type={value.type === "number" ? "number" : "text"}
@@ -92,7 +183,10 @@ const ToolsTab = ({
: e.target.value,
})
}
className="mt-1"
/>
)
}
</div>
),
)}
@@ -100,14 +194,7 @@ const ToolsTab = ({
<Send className="w-4 h-4 mr-2" />
Run Tool
</Button>
{toolResult && (
<>
<h4 className="font-semibold mb-2">Tool Result:</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{toolResult}
</pre>
</>
)}
{toolResult && renderToolResult()}
</div>
) : (
<Alert>

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
@@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
const Alert = React.forwardRef<
HTMLDivElement,
@@ -29,8 +29,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
@@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef<
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
@@ -10,7 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
"bg-primary text-primary-foreground shadow hover:bg-primary/90 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -12,14 +12,14 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,12 +1,12 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@@ -1,19 +1,19 @@
import * as React from "react"
import * as React from "react";
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -23,7 +23,7 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
className,
)}
{...props}
>
@@ -32,8 +32,8 @@ const SelectTrigger = React.forwardRef<
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -43,14 +43,14 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -60,15 +60,15 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@@ -81,7 +81,7 @@ const SelectContent = React.forwardRef<
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@@ -91,7 +91,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
@@ -99,8 +99,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@@ -111,8 +111,8 @@ const SelectLabel = React.forwardRef<
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -122,7 +122,7 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}
>
@@ -133,8 +133,8 @@ const SelectItem = React.forwardRef<
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -145,8 +145,8 @@ const SelectSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -159,4 +159,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
};

View File

@@ -1,9 +1,9 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -13,12 +13,12 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -27,13 +27,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -43,11 +43,11 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
@@ -11,14 +11,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
);
},
);
Textarea.displayName = "Textarea";
export { Textarea }
export { Textarea };

View File

@@ -0,0 +1,19 @@
import {
NotificationSchema as BaseNotificationSchema,
ClientNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
method: z.literal("notifications/stderr"),
params: z.object({
content: z.string(),
}),
});
export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema,
);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>;

View File

@@ -0,0 +1,51 @@
import { useCallback, useEffect, useState } from "react";
type Theme = "light" | "dark" | "system";
const useTheme = (): [Theme, (mode: Theme) => void] => {
const [theme, setTheme] = useState<Theme>(() => {
const savedTheme = localStorage.getItem("theme") as Theme;
return savedTheme || "system";
});
useEffect(() => {
const darkModeMediaQuery = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const handleDarkModeChange = (e: MediaQueryListEvent) => {
if (theme === "system") {
updateDocumentTheme(e.matches ? "dark" : "light");
}
};
const updateDocumentTheme = (newTheme: "light" | "dark") => {
document.documentElement.classList.toggle("dark", newTheme === "dark");
};
// Set initial theme based on current mode
if (theme === "system") {
updateDocumentTheme(darkModeMediaQuery.matches ? "dark" : "light");
} else {
updateDocumentTheme(theme);
}
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
return () => {
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
};
}, [theme]);
return [
theme,
useCallback((newTheme: Theme) => {
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
if (newTheme !== "system") {
document.documentElement.classList.toggle("dark", newTheme === "dark");
}
}, []),
];
};
export default useTheme;

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -1,10 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
<ToastContainer />
</StrictMode>,
);

View File

@@ -1,4 +1,5 @@
/** @type {import('tailwindcss').Config} */
import animate from "tailwindcss-animate";
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
@@ -53,5 +54,5 @@ export default {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [animate],
};

View File

@@ -2,9 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"target": "ES2020",
@@ -25,7 +23,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
},
"include": ["src"]
}

View File

@@ -10,4 +10,12 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false,
rollupOptions: {
output: {
manualChunks: undefined
}
}
}
});

BIN
mcp-inspector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

2579
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,21 @@
{
"name": "mcp-inspector",
"private": true,
"version": "0.0.1",
"name": "@modelcontextprotocol/inspector",
"version": "0.2.4",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector": "./bin/cli.js"
},
"files": [
"bin",
"client/bin",
"client/dist",
"server/build"
],
"workspaces": [
"client",
"server"
@@ -14,11 +27,20 @@
"build": "npm run build-server && npm run build-client",
"start-server": "cd server && npm run start",
"start-client": "cd client && npm run preview",
"start": "concurrently \"npm run start-server\" \"npm run start-client\"",
"prettier-fix": "prettier --write ."
"start": "node ./bin/cli.js",
"prepare": "npm run build",
"prettier-fix": "prettier --write .",
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
},
"dependencies": {
"@modelcontextprotocol/inspector-client": "0.2.4",
"@modelcontextprotocol/inspector-server": "0.2.4",
"concurrently": "^9.0.1",
"spawn-rx": "^5.1.0",
"ts-node": "^10.9.2"
},
"devDependencies": {
"concurrently": "^9.0.1",
"@types/node": "^22.7.5",
"prettier": "3.3.3"
}
}

View File

@@ -1,8 +1,18 @@
{
"name": "mcp-inspector",
"version": "0.0.1",
"main": "build/index.js",
"name": "@modelcontextprotocol/inspector-server",
"version": "0.2.4",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector-server": "build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc",
"start": "node build/index.js",
@@ -17,7 +27,7 @@
"typescript": "^5.6.2"
},
"dependencies": {
"@modelcontextprotocol/sdk": "*",
"@modelcontextprotocol/sdk": "^1.0.1",
"cors": "^2.8.5",
"eventsource": "^2.0.2",
"express": "^4.21.0",

View File

@@ -1,16 +1,31 @@
#!/usr/bin/env node
import cors from "cors";
import EventSource from "eventsource";
import { parseArgs } from "node:util";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import {
StdioClientTransport,
getDefaultEnvironment,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import mcpProxy from "./mcpProxy.js";
import { findActualExecutable } from "spawn-rx";
// Polyfill EventSource for an SSE client in Node.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).EventSource = EventSource;
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
env: { type: "string", default: "" },
args: { type: "string", default: "" },
},
});
const app = express();
app.use(cors());
@@ -23,17 +38,33 @@ const createTransport = async (query: express.Request["query"]) => {
if (transportType === "stdio") {
const command = query.command as string;
const args = (query.args as string).split(/\s+/);
console.log(`Stdio transport: command=${command}, args=${args}`);
const transport = new StdioClientTransport({ command, args });
const origArgs = (query.args as string).split(/\s+/);
const env = query.env ? JSON.parse(query.env as string) : undefined;
const { cmd, args } = findActualExecutable(command, origArgs);
console.log(
`Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
);
const transport = new StdioClientTransport({
command: cmd,
args,
env,
stderr: "pipe",
});
await transport.start();
console.log("Spawned stdio transport");
return transport;
} else if (transportType === "sse") {
const url = query.url as string;
console.log(`SSE transport: url=${url}`);
const transport = new SSEClientTransport(new URL(url));
await transport.start();
console.log("Connected to SSE transport");
return transport;
} else {
@@ -43,6 +74,7 @@ const createTransport = async (query: express.Request["query"]) => {
};
app.get("/sse", async (req, res) => {
try {
console.log("New SSE connection");
const backingServerTransport = await createTransport(req.query);
@@ -57,6 +89,18 @@ app.get("/sse", async (req, res) => {
await webAppTransport.start();
if (backingServerTransport instanceof StdioClientTransport) {
backingServerTransport.stderr!.on("data", (chunk) => {
webAppTransport.send({
jsonrpc: "2.0",
method: "notifications/stderr",
params: {
content: chunk.toString(),
},
});
});
}
mcpProxy({
transportToClient: webAppTransport,
transportToServer: backingServerTransport,
@@ -64,22 +108,45 @@ app.get("/sse", async (req, res) => {
console.error(error);
},
});
console.log("Set up MCP proxy");
} catch (error) {
console.error("Error in /sse route:", error);
res.status(500).json(error);
}
});
app.post("/message", async (req, res) => {
try {
const sessionId = req.query.sessionId;
console.log(`Received message for sessionId ${sessionId}`);
const transport = webAppTransports.find((t) => t.sessionId === sessionId);
if (!transport) {
res.status(404).send("Session not found");
res.status(404).end("Session not found");
return;
}
await transport.handlePostMessage(req, res);
} catch (error) {
console.error("Error in /message route:", error);
res.status(500).json(error);
}
});
app.get("/config", (req, res) => {
try {
const defaultEnvironment = getDefaultEnvironment();
res.json({
defaultEnvironment,
defaultCommand: values.env,
defaultArgs: values.args,
});
} catch (error) {
console.error("Error in /config route:", error);
res.status(500).json(error);
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
app.listen(PORT, () => {});