Merge branch 'main' into hide-run-as-task-if-not-supported
This commit is contained in:
commit
4bcffe456b
19 changed files with 1919 additions and 162 deletions
19
SECURITY.md
19
SECURITY.md
|
|
@ -1,14 +1,21 @@
|
||||||
# Security Policy
|
# Security Policy
|
||||||
Thank you for helping us keep the inspector secure.
|
|
||||||
|
Thank you for helping keep the Model Context Protocol and its ecosystem secure.
|
||||||
|
|
||||||
## Reporting Security Issues
|
## Reporting Security Issues
|
||||||
|
|
||||||
This project is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
|
If you discover a security vulnerability in this repository, please report it through
|
||||||
|
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
|
||||||
|
for this repository.
|
||||||
|
|
||||||
The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
|
Please **do not** report security vulnerabilities through public GitHub issues, discussions,
|
||||||
|
or pull requests.
|
||||||
|
|
||||||
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).
|
## What to Include
|
||||||
|
|
||||||
## Vulnerability Disclosure Program
|
To help us triage and respond quickly, please include:
|
||||||
|
|
||||||
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).
|
- A description of the vulnerability
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- The potential impact
|
||||||
|
- Any suggested fixes (optional)
|
||||||
|
|
|
||||||
216
cli/LICENSE
Normal file
216
cli/LICENSE
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
||||||
|
|
||||||
|
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
|
||||||
|
|
||||||
|
No rights beyond those granted by the applicable original license are conveyed for such contributions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright
|
||||||
|
owner or by an individual or Legal Entity authorized to submit on behalf
|
||||||
|
of the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International (CC-BY-4.0)
|
||||||
|
|
||||||
|
Documentation in this project (excluding specifications) is licensed under
|
||||||
|
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
||||||
|
the full license text.
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"mcp-inspector-cli": "build/cli.js"
|
"mcp-inspector-cli": "build/cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"build"
|
"build",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|
|
||||||
216
client/LICENSE
Normal file
216
client/LICENSE
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
||||||
|
|
||||||
|
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
|
||||||
|
|
||||||
|
No rights beyond those granted by the applicable original license are conveyed for such contributions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright
|
||||||
|
owner or by an individual or Legal Entity authorized to submit on behalf
|
||||||
|
of the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International (CC-BY-4.0)
|
||||||
|
|
||||||
|
Documentation in this project (excluding specifications) is licensed under
|
||||||
|
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
||||||
|
the full license text.
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin",
|
"bin",
|
||||||
"dist"
|
"dist",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 6274",
|
"dev": "vite --port 6274",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
hasValidMetaPrefix,
|
hasValidMetaPrefix,
|
||||||
isReservedMetaKey,
|
isReservedMetaKey,
|
||||||
} from "@/utils/metaUtils";
|
} from "@/utils/metaUtils";
|
||||||
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||||
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
|
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
|
||||||
import { OAuthStateMachine } from "./lib/oauth-state-machine";
|
import { OAuthStateMachine } from "./lib/oauth-state-machine";
|
||||||
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
||||||
|
|
@ -101,6 +102,27 @@ import MetadataTab from "./components/MetadataTab";
|
||||||
|
|
||||||
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
||||||
|
|
||||||
|
type PrefilledAppsToolCall = {
|
||||||
|
id: number;
|
||||||
|
toolName: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
result: CompatibilityCallToolResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAppResourceUri = (tool: Tool): boolean => {
|
||||||
|
return Boolean(getToolUiResourceUri(tool));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneToolParams = (
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> => {
|
||||||
|
try {
|
||||||
|
return structuredClone(source);
|
||||||
|
} catch {
|
||||||
|
return { ...source };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filterReservedMetadata = (
|
const filterReservedMetadata = (
|
||||||
metadata: Record<string, string>,
|
metadata: Record<string, string>,
|
||||||
): Record<string, string> => {
|
): Record<string, string> => {
|
||||||
|
|
@ -137,6 +159,8 @@ const App = () => {
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [toolResult, setToolResult] =
|
const [toolResult, setToolResult] =
|
||||||
useState<CompatibilityCallToolResult | null>(null);
|
useState<CompatibilityCallToolResult | null>(null);
|
||||||
|
const [prefilledAppsToolCall, setPrefilledAppsToolCall] =
|
||||||
|
useState<PrefilledAppsToolCall | null>(null);
|
||||||
const [errors, setErrors] = useState<Record<string, string | null>>({
|
const [errors, setErrors] = useState<Record<string, string | null>>({
|
||||||
resources: null,
|
resources: null,
|
||||||
prompts: null,
|
prompts: null,
|
||||||
|
|
@ -291,6 +315,7 @@ const App = () => {
|
||||||
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
|
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
|
||||||
const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();
|
const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();
|
||||||
const progressTokenRef = useRef(0);
|
const progressTokenRef = useRef(0);
|
||||||
|
const prefilledAppsToolCallIdRef = useRef(0);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>(() => {
|
const [activeTab, setActiveTab] = useState<string>(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
@ -977,7 +1002,7 @@ const App = () => {
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
toolMetadata?: Record<string, unknown>,
|
toolMetadata?: Record<string, unknown>,
|
||||||
runAsTask?: boolean,
|
runAsTask?: boolean,
|
||||||
) => {
|
): Promise<CompatibilityCallToolResult> => {
|
||||||
lastToolCallOriginTabRef.current = currentTabRef.current;
|
lastToolCallOriginTabRef.current = currentTabRef.current;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1045,7 +1070,7 @@ const App = () => {
|
||||||
"_meta" in (response as Record<string, unknown>)
|
"_meta" in (response as Record<string, unknown>)
|
||||||
? ((response as { _meta?: Record<string, unknown> })._meta ?? {})
|
? ((response as { _meta?: Record<string, unknown> })._meta ?? {})
|
||||||
: undefined;
|
: undefined;
|
||||||
setToolResult({
|
let latestToolResult: CompatibilityCallToolResult = {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -1056,7 +1081,8 @@ const App = () => {
|
||||||
...(initialResponseMeta || {}),
|
...(initialResponseMeta || {}),
|
||||||
"io.modelcontextprotocol/related-task": { taskId },
|
"io.modelcontextprotocol/related-task": { taskId },
|
||||||
},
|
},
|
||||||
} as CompatibilityCallToolResult);
|
};
|
||||||
|
setToolResult(latestToolResult);
|
||||||
|
|
||||||
// Polling loop
|
// Polling loop
|
||||||
let taskCompleted = false;
|
let taskCompleted = false;
|
||||||
|
|
@ -1093,12 +1119,13 @@ const App = () => {
|
||||||
CompatibilityCallToolResultSchema,
|
CompatibilityCallToolResultSchema,
|
||||||
);
|
);
|
||||||
console.log(`Result received for task ${taskId}:`, result);
|
console.log(`Result received for task ${taskId}:`, result);
|
||||||
setToolResult(result as CompatibilityCallToolResult);
|
latestToolResult = result as CompatibilityCallToolResult;
|
||||||
|
setToolResult(latestToolResult);
|
||||||
|
|
||||||
// Refresh tasks list to show completed state
|
// Refresh tasks list to show completed state
|
||||||
void listTasks();
|
void listTasks();
|
||||||
} else {
|
} else {
|
||||||
setToolResult({
|
latestToolResult = {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -1106,7 +1133,8 @@ const App = () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
};
|
||||||
|
setToolResult(latestToolResult);
|
||||||
// Refresh tasks list to show failed/cancelled state
|
// Refresh tasks list to show failed/cancelled state
|
||||||
void listTasks();
|
void listTasks();
|
||||||
}
|
}
|
||||||
|
|
@ -1120,7 +1148,7 @@ const App = () => {
|
||||||
? ((response as { _meta?: Record<string, unknown> })._meta ??
|
? ((response as { _meta?: Record<string, unknown> })._meta ??
|
||||||
{})
|
{})
|
||||||
: undefined;
|
: undefined;
|
||||||
setToolResult({
|
latestToolResult = {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -1131,13 +1159,14 @@ const App = () => {
|
||||||
...(pollingResponseMeta || {}),
|
...(pollingResponseMeta || {}),
|
||||||
"io.modelcontextprotocol/related-task": { taskId },
|
"io.modelcontextprotocol/related-task": { taskId },
|
||||||
},
|
},
|
||||||
} as CompatibilityCallToolResult);
|
};
|
||||||
|
setToolResult(latestToolResult);
|
||||||
// Refresh tasks list to show progress
|
// Refresh tasks list to show progress
|
||||||
void listTasks();
|
void listTasks();
|
||||||
}
|
}
|
||||||
} catch (pollingError) {
|
} catch (pollingError) {
|
||||||
console.error("Error polling task status:", pollingError);
|
console.error("Error polling task status:", pollingError);
|
||||||
setToolResult({
|
latestToolResult = {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -1145,16 +1174,22 @@ const App = () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
};
|
||||||
|
setToolResult(latestToolResult);
|
||||||
taskCompleted = true;
|
taskCompleted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsPollingTask(false);
|
setIsPollingTask(false);
|
||||||
|
// Clear any validation errors since tool execution completed
|
||||||
|
setErrors((prev) => ({ ...prev, tools: null }));
|
||||||
|
return latestToolResult;
|
||||||
} else {
|
} else {
|
||||||
setToolResult(response as CompatibilityCallToolResult);
|
const directResult = response as CompatibilityCallToolResult;
|
||||||
|
setToolResult(directResult);
|
||||||
|
// Clear any validation errors since tool execution completed
|
||||||
|
setErrors((prev) => ({ ...prev, tools: null }));
|
||||||
|
return directResult;
|
||||||
}
|
}
|
||||||
// Clear any validation errors since tool execution completed
|
|
||||||
setErrors((prev) => ({ ...prev, tools: null }));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const toolResult: CompatibilityCallToolResult = {
|
const toolResult: CompatibilityCallToolResult = {
|
||||||
content: [
|
content: [
|
||||||
|
|
@ -1168,6 +1203,7 @@ const App = () => {
|
||||||
setToolResult(toolResult);
|
setToolResult(toolResult);
|
||||||
// Clear validation errors - tool execution errors are shown in ToolResults
|
// Clear validation errors - tool execution errors are shown in ToolResults
|
||||||
setErrors((prev) => ({ ...prev, tools: null }));
|
setErrors((prev) => ({ ...prev, tools: null }));
|
||||||
|
return toolResult;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1508,7 +1544,26 @@ const App = () => {
|
||||||
) => {
|
) => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
setToolResult(null);
|
setToolResult(null);
|
||||||
await callTool(name, params, metadata, runAsTask);
|
const result = await callTool(
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
metadata,
|
||||||
|
runAsTask,
|
||||||
|
);
|
||||||
|
const calledTool = tools.find(
|
||||||
|
(tool) => tool.name === name,
|
||||||
|
);
|
||||||
|
if (calledTool && hasAppResourceUri(calledTool)) {
|
||||||
|
setPrefilledAppsToolCall({
|
||||||
|
id: ++prefilledAppsToolCallIdRef.current,
|
||||||
|
toolName: name,
|
||||||
|
params: cloneToolParams(params),
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPrefilledAppsToolCall(null);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}}
|
}}
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
setSelectedTool={(tool) => {
|
setSelectedTool={(tool) => {
|
||||||
|
|
@ -1552,6 +1607,22 @@ const App = () => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
listTools();
|
listTools();
|
||||||
}}
|
}}
|
||||||
|
callTool={async (
|
||||||
|
name: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
runAsTask?: boolean,
|
||||||
|
) => {
|
||||||
|
clearError("tools");
|
||||||
|
setToolResult(null);
|
||||||
|
return callTool(name, params, metadata, runAsTask);
|
||||||
|
}}
|
||||||
|
prefilledToolCall={prefilledAppsToolCall}
|
||||||
|
onPrefilledToolCallConsumed={(callId) => {
|
||||||
|
setPrefilledAppsToolCall((prev) =>
|
||||||
|
prev?.id === callId ? null : prev,
|
||||||
|
);
|
||||||
|
}}
|
||||||
error={errors.tools}
|
error={errors.tools}
|
||||||
mcpClient={mcpClient}
|
mcpClient={mcpClient}
|
||||||
onNotification={(notification) => {
|
onNotification={(notification) => {
|
||||||
|
|
|
||||||
284
client/src/__tests__/App.toolsAppsPrefill.test.tsx
Normal file
284
client/src/__tests__/App.toolsAppsPrefill.test.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import App from "../App";
|
||||||
|
import { useConnection } from "../lib/hooks/useConnection";
|
||||||
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
||||||
|
type ToolListEntry = {
|
||||||
|
name: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
_meta: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppsTabProps = {
|
||||||
|
tools: ToolListEntry[];
|
||||||
|
prefilledToolCall?: {
|
||||||
|
id: number;
|
||||||
|
toolName: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
result: {
|
||||||
|
content: Array<{ type: string; text: string }>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
onPrefilledToolCallConsumed?: (callId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock auth dependencies first
|
||||||
|
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||||
|
auth: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../lib/oauth-state-machine", () => ({
|
||||||
|
OAuthStateMachine: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../lib/auth", () => ({
|
||||||
|
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||||
|
tokens: jest.fn().mockResolvedValue(null),
|
||||||
|
clear: jest.fn(),
|
||||||
|
})),
|
||||||
|
DebugInspectorOAuthClientProvider: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../utils/configUtils", () => ({
|
||||||
|
...jest.requireActual("../utils/configUtils"),
|
||||||
|
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
|
||||||
|
getMCPProxyAuthToken: jest.fn(() => ({
|
||||||
|
token: "",
|
||||||
|
header: "X-MCP-Proxy-Auth",
|
||||||
|
})),
|
||||||
|
getInitialTransportType: jest.fn(() => "stdio"),
|
||||||
|
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
|
||||||
|
getInitialCommand: jest.fn(() => "mcp-server-everything"),
|
||||||
|
getInitialArgs: jest.fn(() => ""),
|
||||||
|
initializeInspectorConfig: jest.fn(() => ({})),
|
||||||
|
saveInspectorConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../lib/hooks/useDraggablePane", () => ({
|
||||||
|
useDraggablePane: () => ({
|
||||||
|
height: 300,
|
||||||
|
handleDragStart: jest.fn(),
|
||||||
|
}),
|
||||||
|
useDraggableSidebar: () => ({
|
||||||
|
width: 320,
|
||||||
|
isDragging: false,
|
||||||
|
handleDragStart: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/Sidebar", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>Sidebar</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/ResourcesTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>ResourcesTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/PromptsTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>PromptsTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/TasksTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>TasksTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/ConsoleTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>ConsoleTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/PingTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>PingTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/SamplingTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>SamplingTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/RootsTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>RootsTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/ElicitationTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>ElicitationTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/MetadataTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>MetadataTab</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/AuthDebugger", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>AuthDebugger</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/HistoryAndNotifications", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div>HistoryAndNotifications</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/ToolsTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({
|
||||||
|
listTools,
|
||||||
|
callTool,
|
||||||
|
}: {
|
||||||
|
listTools: () => void;
|
||||||
|
callTool: (
|
||||||
|
name: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
runAsTask?: boolean,
|
||||||
|
) => Promise<unknown>;
|
||||||
|
}) => (
|
||||||
|
<div data-testid="tools-tab">
|
||||||
|
<button type="button" onClick={listTools}>
|
||||||
|
mock list tools
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void callTool("weatherApp", { city: "Lisbon" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
mock run app tool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../components/AppsTab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (props: AppsTabProps) => {
|
||||||
|
const prefilled =
|
||||||
|
props && "prefilledToolCall" in props ? props.prefilledToolCall : null;
|
||||||
|
const tools = props && "tools" in props ? props.tools : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="apps-tab">
|
||||||
|
<div data-testid="apps-tools">{JSON.stringify(tools)}</div>
|
||||||
|
<div data-testid="apps-prefilled">
|
||||||
|
{JSON.stringify(prefilled ?? null)}
|
||||||
|
</div>
|
||||||
|
{prefilled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props?.onPrefilledToolCallConsumed?.(prefilled.id)}
|
||||||
|
>
|
||||||
|
consume prefilled
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });
|
||||||
|
|
||||||
|
jest.mock("../lib/hooks/useConnection", () => ({
|
||||||
|
useConnection: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("App - Tools to Apps prefilled handoff", () => {
|
||||||
|
const mockUseConnection = jest.mocked(useConnection);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
window.location.hash = "#tools";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes prefilled call data to AppsTab for tools using _meta['ui/resourceUri']", async () => {
|
||||||
|
const makeRequest = jest.fn(async (request: { method: string }) => {
|
||||||
|
if (request.method === "tools/list") {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "weatherApp",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
"ui/resourceUri": "ui://weather-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextCursor: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "tools/call") {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "weather result" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected method: ${request.method}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUseConnection.mockReturnValue({
|
||||||
|
connectionStatus: "connected",
|
||||||
|
serverCapabilities: { tools: { listChanged: true } },
|
||||||
|
serverImplementation: null,
|
||||||
|
mcpClient: {
|
||||||
|
request: jest.fn(),
|
||||||
|
notification: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
} as unknown as Client,
|
||||||
|
requestHistory: [],
|
||||||
|
clearRequestHistory: jest.fn(),
|
||||||
|
makeRequest,
|
||||||
|
cancelTask: jest.fn(),
|
||||||
|
listTasks: jest.fn(),
|
||||||
|
sendNotification: jest.fn(),
|
||||||
|
handleCompletion: jest.fn(),
|
||||||
|
completionsSupported: false,
|
||||||
|
connect: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
} as ReturnType<typeof useConnection>);
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /mock list tools/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const tools = JSON.parse(
|
||||||
|
screen.getByTestId("apps-tools").textContent || "[]",
|
||||||
|
);
|
||||||
|
expect(tools).toHaveLength(1);
|
||||||
|
expect(tools[0].name).toBe("weatherApp");
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /mock run app tool/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const prefilled = JSON.parse(
|
||||||
|
screen.getByTestId("apps-prefilled").textContent || "null",
|
||||||
|
);
|
||||||
|
expect(prefilled.toolName).toBe("weatherApp");
|
||||||
|
expect(prefilled.params).toEqual({ city: "Lisbon" });
|
||||||
|
expect(prefilled.result.content[0].text).toBe("weather result");
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /consume prefilled/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("apps-prefilled")).toHaveTextContent("null");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,6 +3,9 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import {
|
import {
|
||||||
Tool,
|
Tool,
|
||||||
ContentBlock,
|
ContentBlock,
|
||||||
|
CompatibilityCallToolResult,
|
||||||
|
CallToolResult,
|
||||||
|
CallToolResultSchema,
|
||||||
ServerNotification,
|
ServerNotification,
|
||||||
LoggingMessageNotificationParams,
|
LoggingMessageNotificationParams,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
@ -24,6 +27,7 @@ interface AppRendererProps {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
mcpClient: Client | null;
|
mcpClient: Client | null;
|
||||||
toolInput?: Record<string, unknown>;
|
toolInput?: Record<string, unknown>;
|
||||||
|
toolResult?: CompatibilityCallToolResult | null;
|
||||||
onNotification?: (notification: ServerNotification) => void;
|
onNotification?: (notification: ServerNotification) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,11 +36,32 @@ const AppRenderer = ({
|
||||||
tool,
|
tool,
|
||||||
mcpClient,
|
mcpClient,
|
||||||
toolInput,
|
toolInput,
|
||||||
|
toolResult,
|
||||||
onNotification,
|
onNotification,
|
||||||
}: AppRendererProps) => {
|
}: AppRendererProps) => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const normalizedToolResult = useMemo<CallToolResult | undefined>(() => {
|
||||||
|
if (!toolResult) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("content" in toolResult) {
|
||||||
|
const parsedResult = CallToolResultSchema.safeParse(toolResult);
|
||||||
|
return parsedResult.success ? parsedResult.data : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("toolResult" in toolResult) {
|
||||||
|
const parsedResult = CallToolResultSchema.safeParse(
|
||||||
|
toolResult.toolResult,
|
||||||
|
);
|
||||||
|
return parsedResult.success ? parsedResult.data : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [toolResult]);
|
||||||
|
|
||||||
const hostContext: McpUiHostContext = useMemo(
|
const hostContext: McpUiHostContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
theme: document.documentElement.classList.contains("dark")
|
theme: document.documentElement.classList.contains("dark")
|
||||||
|
|
@ -115,6 +140,7 @@ const AppRenderer = ({
|
||||||
toolName={tool.name}
|
toolName={tool.name}
|
||||||
hostContext={hostContext}
|
hostContext={hostContext}
|
||||||
toolInput={toolInput}
|
toolInput={toolInput}
|
||||||
|
toolResult={normalizedToolResult}
|
||||||
sandbox={{
|
sandbox={{
|
||||||
url: new URL(sandboxPath, window.location.origin),
|
url: new URL(sandboxPath, window.location.origin),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,16 @@ import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
X,
|
X,
|
||||||
Play,
|
Play,
|
||||||
|
Loader2,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
Minimize2,
|
Minimize2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Tool, ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
import {
|
||||||
|
Tool,
|
||||||
|
ServerNotification,
|
||||||
|
CompatibilityCallToolResult,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||||
import AppRenderer from "./AppRenderer";
|
import AppRenderer from "./AppRenderer";
|
||||||
|
|
@ -40,6 +45,19 @@ interface AppsTabProps {
|
||||||
sandboxPath: string;
|
sandboxPath: string;
|
||||||
tools: Tool[];
|
tools: Tool[];
|
||||||
listTools: () => void;
|
listTools: () => void;
|
||||||
|
callTool: (
|
||||||
|
name: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
runAsTask?: boolean,
|
||||||
|
) => Promise<CompatibilityCallToolResult>;
|
||||||
|
prefilledToolCall?: {
|
||||||
|
id: number;
|
||||||
|
toolName: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
result: CompatibilityCallToolResult;
|
||||||
|
} | null;
|
||||||
|
onPrefilledToolCallConsumed?: (callId: number) => void;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
mcpClient: Client | null;
|
mcpClient: Client | null;
|
||||||
onNotification?: (notification: ServerNotification) => void;
|
onNotification?: (notification: ServerNotification) => void;
|
||||||
|
|
@ -50,10 +68,23 @@ const hasUIMetadata = (tool: Tool): boolean => {
|
||||||
return !!getToolUiResourceUri(tool);
|
return !!getToolUiResourceUri(tool);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cloneToolParams = (
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> => {
|
||||||
|
try {
|
||||||
|
return structuredClone(source);
|
||||||
|
} catch {
|
||||||
|
return { ...source };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const AppsTab = ({
|
const AppsTab = ({
|
||||||
sandboxPath,
|
sandboxPath,
|
||||||
tools,
|
tools,
|
||||||
listTools,
|
listTools,
|
||||||
|
callTool,
|
||||||
|
prefilledToolCall,
|
||||||
|
onPrefilledToolCallConsumed,
|
||||||
error,
|
error,
|
||||||
mcpClient,
|
mcpClient,
|
||||||
onNotification,
|
onNotification,
|
||||||
|
|
@ -62,9 +93,38 @@ const AppsTab = ({
|
||||||
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
const [isAppOpen, setIsAppOpen] = useState(false);
|
const [isAppOpen, setIsAppOpen] = useState(false);
|
||||||
|
const [isOpeningApp, setIsOpeningApp] = useState(false);
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
const [hasValidationErrors, setHasValidationErrors] = useState(false);
|
||||||
|
const [submittedParams, setSubmittedParams] = useState<
|
||||||
|
Record<string, unknown> | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [submittedToolResult, setSubmittedToolResult] =
|
||||||
|
useState<CompatibilityCallToolResult | null>(null);
|
||||||
const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});
|
const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});
|
||||||
|
const openAppRunIdRef = useRef(0);
|
||||||
|
const prefillingParamsRef = useRef<Record<string, unknown> | null>(null);
|
||||||
|
const consumedPrefilledCallIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const buildInitialParams = useCallback((tool: Tool) => {
|
||||||
|
const initialParams = Object.entries(tool.inputSchema.properties ?? []).map(
|
||||||
|
([key, value]) => {
|
||||||
|
const resolvedValue = resolveRef(
|
||||||
|
value as JsonSchemaType,
|
||||||
|
tool.inputSchema as JsonSchemaType,
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
generateDefaultValue(
|
||||||
|
resolvedValue,
|
||||||
|
key,
|
||||||
|
tool.inputSchema as JsonSchemaType,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Object.fromEntries(initialParams);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Function to check if any form has validation errors
|
// Function to check if any form has validation errors
|
||||||
const checkValidationErrors = useCallback(() => {
|
const checkValidationErrors = useCallback(() => {
|
||||||
|
|
@ -89,63 +149,145 @@ const AppsTab = ({
|
||||||
if (selectedTool && !filtered.find((t) => t.name === selectedTool.name)) {
|
if (selectedTool && !filtered.find((t) => t.name === selectedTool.name)) {
|
||||||
setSelectedTool(null);
|
setSelectedTool(null);
|
||||||
setIsAppOpen(false);
|
setIsAppOpen(false);
|
||||||
|
setSubmittedParams(undefined);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
}
|
}
|
||||||
}, [tools, selectedTool]);
|
}, [tools, selectedTool]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTool) {
|
if (selectedTool) {
|
||||||
const initialParams = Object.entries(
|
const prefillingParams = prefillingParamsRef.current;
|
||||||
selectedTool.inputSchema.properties ?? [],
|
if (prefillingParams) {
|
||||||
).map(([key, value]) => {
|
setParams(prefillingParams);
|
||||||
// First resolve any $ref references
|
prefillingParamsRef.current = null;
|
||||||
const resolvedValue = resolveRef(
|
} else {
|
||||||
value as JsonSchemaType,
|
setParams(buildInitialParams(selectedTool));
|
||||||
selectedTool.inputSchema as JsonSchemaType,
|
}
|
||||||
);
|
|
||||||
return [
|
|
||||||
key,
|
|
||||||
generateDefaultValue(
|
|
||||||
resolvedValue,
|
|
||||||
key,
|
|
||||||
selectedTool.inputSchema as JsonSchemaType,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
setParams(Object.fromEntries(initialParams));
|
|
||||||
setHasValidationErrors(false);
|
setHasValidationErrors(false);
|
||||||
formRefs.current = {};
|
formRefs.current = {};
|
||||||
} else {
|
} else {
|
||||||
setParams({});
|
setParams({});
|
||||||
setIsAppOpen(false);
|
setIsAppOpen(false);
|
||||||
|
setSubmittedParams(undefined);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
}
|
}
|
||||||
}, [selectedTool]);
|
}, [buildInitialParams, selectedTool]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!prefilledToolCall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumedPrefilledCallIdRef.current === prefilledToolCall.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingTool = appTools.find(
|
||||||
|
(tool) => tool.name === prefilledToolCall.toolName,
|
||||||
|
);
|
||||||
|
if (!matchingTool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydratedParams = cloneToolParams(prefilledToolCall.params);
|
||||||
|
|
||||||
|
openAppRunIdRef.current += 1;
|
||||||
|
setIsOpeningApp(false);
|
||||||
|
prefillingParamsRef.current = hydratedParams;
|
||||||
|
setSelectedTool(matchingTool);
|
||||||
|
setSubmittedParams(hydratedParams);
|
||||||
|
setSubmittedToolResult(prefilledToolCall.result);
|
||||||
|
setIsAppOpen(true);
|
||||||
|
setIsMaximized(false);
|
||||||
|
consumedPrefilledCallIdRef.current = prefilledToolCall.id;
|
||||||
|
onPrefilledToolCallConsumed?.(prefilledToolCall.id);
|
||||||
|
}, [appTools, onPrefilledToolCallConsumed, prefilledToolCall]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
listTools();
|
listTools();
|
||||||
}, [listTools]);
|
}, [listTools]);
|
||||||
|
|
||||||
|
const executeToolAndOpenApp = useCallback(
|
||||||
|
async (tool: Tool, toolParams: Record<string, unknown>) => {
|
||||||
|
const runId = ++openAppRunIdRef.current;
|
||||||
|
const runParams = cloneToolParams(toolParams);
|
||||||
|
prefillingParamsRef.current = null;
|
||||||
|
setIsOpeningApp(true);
|
||||||
|
setSubmittedParams(runParams);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
|
try {
|
||||||
|
const result = await callTool(tool.name, runParams);
|
||||||
|
|
||||||
|
if (runId !== openAppRunIdRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmittedParams(runParams);
|
||||||
|
setSubmittedToolResult(result);
|
||||||
|
setIsAppOpen(true);
|
||||||
|
} catch {
|
||||||
|
if (runId !== openAppRunIdRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmittedToolResult(null);
|
||||||
|
setIsAppOpen(false);
|
||||||
|
} finally {
|
||||||
|
if (runId === openAppRunIdRef.current) {
|
||||||
|
setIsOpeningApp(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[callTool],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCloseApp = useCallback(() => {
|
const handleCloseApp = useCallback(() => {
|
||||||
|
openAppRunIdRef.current += 1;
|
||||||
|
setIsOpeningApp(false);
|
||||||
setIsAppOpen(false);
|
setIsAppOpen(false);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpenApp = useCallback(() => {
|
const handleOpenApp = useCallback(async () => {
|
||||||
if (!checkValidationErrors()) {
|
if (!selectedTool || checkValidationErrors()) {
|
||||||
setIsAppOpen(true);
|
return;
|
||||||
}
|
}
|
||||||
}, [checkValidationErrors]);
|
|
||||||
|
|
||||||
const handleSelectTool = useCallback((tool: Tool) => {
|
await executeToolAndOpenApp(selectedTool, params);
|
||||||
setSelectedTool(tool);
|
}, [checkValidationErrors, executeToolAndOpenApp, params, selectedTool]);
|
||||||
const hasFields =
|
|
||||||
tool.inputSchema.properties &&
|
const handleSelectTool = useCallback(
|
||||||
Object.keys(tool.inputSchema.properties).length > 0;
|
(tool: Tool) => {
|
||||||
setIsAppOpen(!hasFields);
|
openAppRunIdRef.current += 1;
|
||||||
}, []);
|
setIsOpeningApp(false);
|
||||||
|
prefillingParamsRef.current = null;
|
||||||
|
setSelectedTool(tool);
|
||||||
|
setSubmittedParams(undefined);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
|
const hasFields =
|
||||||
|
tool.inputSchema.properties &&
|
||||||
|
Object.keys(tool.inputSchema.properties).length > 0;
|
||||||
|
|
||||||
|
if (hasFields) {
|
||||||
|
setIsAppOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialParams = buildInitialParams(tool);
|
||||||
|
void executeToolAndOpenApp(tool, initialParams);
|
||||||
|
},
|
||||||
|
[buildInitialParams, executeToolAndOpenApp],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeselectTool = useCallback(() => {
|
const handleDeselectTool = useCallback(() => {
|
||||||
|
openAppRunIdRef.current += 1;
|
||||||
|
setIsOpeningApp(false);
|
||||||
|
prefillingParamsRef.current = null;
|
||||||
setSelectedTool(null);
|
setSelectedTool(null);
|
||||||
setIsAppOpen(false);
|
setIsAppOpen(false);
|
||||||
setIsMaximized(false);
|
setIsMaximized(false);
|
||||||
|
setSubmittedParams(undefined);
|
||||||
|
setSubmittedToolResult(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -470,12 +612,16 @@ const AppsTab = ({
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenApp}
|
onClick={() => void handleOpenApp()}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={hasValidationErrors}
|
disabled={hasValidationErrors || isOpeningApp}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4 mr-2" />
|
{isOpeningApp ? (
|
||||||
Open App
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isOpeningApp ? "Opening App..." : "Open App"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -497,7 +643,8 @@ const AppsTab = ({
|
||||||
sandboxPath={sandboxPath}
|
sandboxPath={sandboxPath}
|
||||||
tool={selectedTool}
|
tool={selectedTool}
|
||||||
mcpClient={mcpClient}
|
mcpClient={mcpClient}
|
||||||
toolInput={params}
|
toolInput={submittedParams}
|
||||||
|
toolResult={submittedToolResult}
|
||||||
onNotification={onNotification}
|
onNotification={onNotification}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
import { validateRedirectUrl } from "@/utils/urlValidation";
|
import { validateRedirectUrl } from "@/utils/urlValidation";
|
||||||
import { useToast } from "@/lib/hooks/useToast";
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
|
import { getAuthorizationServerMetadataDiscoveryUrl } from "@/utils/oauthUtils";
|
||||||
|
|
||||||
interface OAuthStepProps {
|
interface OAuthStepProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -81,6 +82,13 @@ export const OAuthFlowProgress = ({
|
||||||
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
|
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const authorizationServerMetadataDiscoveryUrl = useMemo(() => {
|
||||||
|
if (!authState.authServerUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl);
|
||||||
|
}, [authState.authServerUrl]);
|
||||||
|
|
||||||
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
|
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
|
||||||
|
|
||||||
|
|
@ -197,13 +205,7 @@ export const OAuthFlowProgress = ({
|
||||||
<p className="font-medium">Authorization Server Metadata:</p>
|
<p className="font-medium">Authorization Server Metadata:</p>
|
||||||
{authState.authServerUrl && (
|
{authState.authServerUrl && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
From{" "}
|
From {authorizationServerMetadataDiscoveryUrl}
|
||||||
{
|
|
||||||
new URL(
|
|
||||||
"/.well-known/oauth-authorization-server",
|
|
||||||
authState.authServerUrl,
|
|
||||||
).href
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
CompatibilityCallToolResult,
|
CompatibilityCallToolResult,
|
||||||
ListToolsResult,
|
ListToolsResult,
|
||||||
Tool,
|
Tool,
|
||||||
|
ToolAnnotations,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
|
|
@ -85,6 +86,84 @@ const getTaskSupport = (
|
||||||
return "optional";
|
return "optional";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Type guard to safely detect the optional annotations field
|
||||||
|
const hasAnnotations = (
|
||||||
|
tool: Tool,
|
||||||
|
): tool is Tool & { annotations: ToolAnnotations } =>
|
||||||
|
typeof (tool as { annotations?: unknown }).annotations !== "undefined" &&
|
||||||
|
(tool as { annotations?: unknown }).annotations !== null;
|
||||||
|
|
||||||
|
// Helper to render annotation badges
|
||||||
|
// Shows all 4 annotation values with their state (true/false/implied default)
|
||||||
|
const AnnotationBadges = ({
|
||||||
|
annotations,
|
||||||
|
}: {
|
||||||
|
annotations: ToolAnnotations | undefined;
|
||||||
|
}) => {
|
||||||
|
// Spec defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true
|
||||||
|
const getValueAndImplied = (
|
||||||
|
value: boolean | undefined,
|
||||||
|
defaultValue: boolean,
|
||||||
|
): { value: boolean; implied: boolean } => ({
|
||||||
|
value: value ?? defaultValue,
|
||||||
|
implied: value === undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readOnly = getValueAndImplied(annotations?.readOnlyHint, false);
|
||||||
|
const destructive = getValueAndImplied(annotations?.destructiveHint, true);
|
||||||
|
const idempotent = getValueAndImplied(annotations?.idempotentHint, false);
|
||||||
|
const openWorld = getValueAndImplied(annotations?.openWorldHint, true);
|
||||||
|
|
||||||
|
// Descriptions from MCP spec
|
||||||
|
const badges = [
|
||||||
|
{
|
||||||
|
label: "Read-only",
|
||||||
|
value: readOnly.value,
|
||||||
|
implied: readOnly.implied,
|
||||||
|
description: "Tool does not modify its environment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Destructive",
|
||||||
|
value: destructive.value,
|
||||||
|
implied: destructive.implied,
|
||||||
|
description:
|
||||||
|
"Tool may perform destructive updates (delete/overwrite data)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Idempotent",
|
||||||
|
value: idempotent.value,
|
||||||
|
implied: idempotent.implied,
|
||||||
|
description: "Calling repeatedly with same args has no additional effect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open-world",
|
||||||
|
value: openWorld.value,
|
||||||
|
implied: openWorld.implied,
|
||||||
|
description:
|
||||||
|
"Tool may interact with external entities beyond its local environment",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{badges.map(({ label, value, implied, description }) => (
|
||||||
|
<span
|
||||||
|
key={label}
|
||||||
|
title={`${description}\n\nValue: ${value ? "Yes" : "No"} (${implied ? "implied default" : "explicitly set"})`}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border",
|
||||||
|
"bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-600",
|
||||||
|
implied && "border-dashed opacity-60",
|
||||||
|
!implied && "border-solid",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value ? "✓" : "✗"} {label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ToolsTab = ({
|
const ToolsTab = ({
|
||||||
tools,
|
tools,
|
||||||
listTools,
|
listTools,
|
||||||
|
|
@ -107,7 +186,7 @@ const ToolsTab = ({
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
metadata?: Record<string, unknown>,
|
metadata?: Record<string, unknown>,
|
||||||
runAsTask?: boolean,
|
runAsTask?: boolean,
|
||||||
) => Promise<void>;
|
) => Promise<CompatibilityCallToolResult>;
|
||||||
selectedTool: Tool | null;
|
selectedTool: Tool | null;
|
||||||
setSelectedTool: (tool: Tool | null) => void;
|
setSelectedTool: (tool: Tool | null) => void;
|
||||||
toolResult: CompatibilityCallToolResult | null;
|
toolResult: CompatibilityCallToolResult | null;
|
||||||
|
|
@ -247,6 +326,13 @@ const ToolsTab = ({
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||||
{selectedTool.description}
|
{selectedTool.description}
|
||||||
</p>
|
</p>
|
||||||
|
<AnnotationBadges
|
||||||
|
annotations={
|
||||||
|
hasAnnotations(selectedTool)
|
||||||
|
? selectedTool.annotations
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
||||||
([key, value]) => {
|
([key, value]) => {
|
||||||
// First resolve any $ref references
|
// First resolve any $ref references
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,33 @@
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { describe, it, jest, beforeEach } from "@jest/globals";
|
import { describe, it, jest, beforeEach } from "@jest/globals";
|
||||||
import AppRenderer from "../AppRenderer";
|
import AppRenderer from "../AppRenderer";
|
||||||
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
import {
|
||||||
|
Tool,
|
||||||
|
CompatibilityCallToolResult,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { RequestHandlerExtra } from "@mcp-ui/client";
|
import { RequestHandlerExtra } from "@mcp-ui/client";
|
||||||
import { McpUiMessageResult } from "@modelcontextprotocol/ext-apps";
|
import { McpUiMessageResult } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
|
type BridgeEvent = {
|
||||||
|
type: "sendToolInput" | "sendToolResult";
|
||||||
|
toolName: string;
|
||||||
|
payload: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MockMcpUiRendererProps = {
|
||||||
|
toolName: string;
|
||||||
|
toolInput?: Record<string, unknown>;
|
||||||
|
toolResult?: CompatibilityCallToolResult;
|
||||||
|
onMessage?: (
|
||||||
|
params: { role: "user"; content: { type: "text"; text: string }[] },
|
||||||
|
extra: RequestHandlerExtra,
|
||||||
|
) => Promise<McpUiMessageResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBridgeEvents: BridgeEvent[] = [];
|
||||||
|
|
||||||
// Mock the ext-apps module
|
// Mock the ext-apps module
|
||||||
jest.mock("@modelcontextprotocol/ext-apps/app-bridge", () => ({
|
jest.mock("@modelcontextprotocol/ext-apps/app-bridge", () => ({
|
||||||
getToolUiResourceUri: (tool: Tool) => {
|
getToolUiResourceUri: (tool: Tool) => {
|
||||||
|
|
@ -24,34 +45,74 @@ jest.mock("@/lib/hooks/useToast", () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock @mcp-ui/client
|
// Mock @mcp-ui/client to simulate bridge delivery only after app initialization
|
||||||
jest.mock("@mcp-ui/client", () => ({
|
jest.mock("@mcp-ui/client", () => {
|
||||||
AppRenderer: ({
|
const React = jest.requireActual("react") as typeof import("react");
|
||||||
toolName,
|
|
||||||
onMessage,
|
return {
|
||||||
}: {
|
AppRenderer: ({
|
||||||
toolName: string;
|
toolName,
|
||||||
onMessage?: (
|
toolInput,
|
||||||
params: { role: "user"; content: { type: "text"; text: string }[] },
|
toolResult,
|
||||||
extra: RequestHandlerExtra,
|
onMessage,
|
||||||
) => Promise<McpUiMessageResult>;
|
}: MockMcpUiRendererProps) => {
|
||||||
}) => (
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||||
<div data-testid="mcp-ui-app-renderer">
|
|
||||||
<div data-testid="tool-name">{toolName}</div>
|
React.useEffect(() => {
|
||||||
<button
|
setIsInitialized(false);
|
||||||
data-testid="trigger-message"
|
}, [toolName]);
|
||||||
onClick={() =>
|
|
||||||
onMessage?.(
|
React.useEffect(() => {
|
||||||
{ role: "user", content: [{ type: "text", text: "Test message" }] },
|
if (isInitialized && toolInput) {
|
||||||
{} as RequestHandlerExtra,
|
mockBridgeEvents.push({
|
||||||
)
|
type: "sendToolInput",
|
||||||
|
toolName,
|
||||||
|
payload: toolInput,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
>
|
}, [isInitialized, toolInput, toolName]);
|
||||||
Trigger Message
|
|
||||||
</button>
|
React.useEffect(() => {
|
||||||
</div>
|
if (isInitialized && toolResult) {
|
||||||
),
|
mockBridgeEvents.push({
|
||||||
}));
|
type: "sendToolResult",
|
||||||
|
toolName,
|
||||||
|
payload: toolResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isInitialized, toolResult, toolName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="mcp-ui-app-renderer">
|
||||||
|
<div data-testid="tool-name">{toolName}</div>
|
||||||
|
<div data-testid="tool-result">
|
||||||
|
{JSON.stringify(toolResult ?? null)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
data-testid="initialize-app"
|
||||||
|
onClick={() => setIsInitialized(true)}
|
||||||
|
>
|
||||||
|
Initialize App
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="trigger-message"
|
||||||
|
onClick={() =>
|
||||||
|
onMessage?.(
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Test message" }],
|
||||||
|
},
|
||||||
|
{} as RequestHandlerExtra,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Trigger Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("AppRenderer", () => {
|
describe("AppRenderer", () => {
|
||||||
const mockTool: Tool = {
|
const mockTool: Tool = {
|
||||||
|
|
@ -69,7 +130,6 @@ describe("AppRenderer", () => {
|
||||||
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
|
||||||
const mockMcpClient = {
|
const mockMcpClient = {
|
||||||
request: jest.fn(),
|
|
||||||
getServerCapabilities: jest.fn().mockReturnValue({}),
|
getServerCapabilities: jest.fn().mockReturnValue({}),
|
||||||
setNotificationHandler: jest.fn(),
|
setNotificationHandler: jest.fn(),
|
||||||
} as unknown as Client;
|
} as unknown as Client;
|
||||||
|
|
@ -78,10 +138,12 @@ describe("AppRenderer", () => {
|
||||||
sandboxPath: "/sandbox",
|
sandboxPath: "/sandbox",
|
||||||
tool: mockTool,
|
tool: mockTool,
|
||||||
mcpClient: mockMcpClient,
|
mcpClient: mockMcpClient,
|
||||||
|
toolResult: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockBridgeEvents.length = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display waiting state when mcpClient is null", () => {
|
it("should display waiting state when mcpClient is null", () => {
|
||||||
|
|
@ -112,4 +174,91 @@ describe("AppRenderer", () => {
|
||||||
description: "Test message",
|
description: "Test message",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should send provided tool input and tool result after app initialization", async () => {
|
||||||
|
const result: CompatibilityCallToolResult = {
|
||||||
|
content: [{ type: "text", text: "Budget initialized" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AppRenderer
|
||||||
|
{...defaultProps}
|
||||||
|
toolInput={{ monthlyBudget: 2500 }}
|
||||||
|
toolResult={result}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockBridgeEvents).toHaveLength(0);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("initialize-app"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const toolEvents = mockBridgeEvents.filter(
|
||||||
|
(event) => event.toolName === "testApp",
|
||||||
|
);
|
||||||
|
expect(toolEvents.map((event) => event.type)).toEqual([
|
||||||
|
"sendToolInput",
|
||||||
|
"sendToolResult",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultEvent = mockBridgeEvents.find(
|
||||||
|
(event) =>
|
||||||
|
event.toolName === "testApp" && event.type === "sendToolResult",
|
||||||
|
);
|
||||||
|
expect(resultEvent?.payload).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not send tool result event when toolResult is null", async () => {
|
||||||
|
render(
|
||||||
|
<AppRenderer
|
||||||
|
{...defaultProps}
|
||||||
|
toolInput={{ monthlyBudget: 2500 }}
|
||||||
|
toolResult={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("initialize-app"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const inputEvents = mockBridgeEvents.filter(
|
||||||
|
(event) =>
|
||||||
|
event.toolName === "testApp" && event.type === "sendToolInput",
|
||||||
|
);
|
||||||
|
expect(inputEvents).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultEvents = mockBridgeEvents.filter(
|
||||||
|
(event) =>
|
||||||
|
event.toolName === "testApp" && event.type === "sendToolResult",
|
||||||
|
);
|
||||||
|
expect(resultEvents).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize compatibility wrapper tool results before sending", async () => {
|
||||||
|
const wrappedResult = {
|
||||||
|
toolResult: {
|
||||||
|
content: [{ type: "text", text: "Wrapped result payload" }],
|
||||||
|
},
|
||||||
|
} as CompatibilityCallToolResult;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AppRenderer
|
||||||
|
{...defaultProps}
|
||||||
|
toolInput={{ monthlyBudget: 2500 }}
|
||||||
|
toolResult={wrappedResult}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("initialize-app"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const resultEvent = mockBridgeEvents.find(
|
||||||
|
(event) =>
|
||||||
|
event.toolName === "testApp" && event.type === "sendToolResult",
|
||||||
|
);
|
||||||
|
expect(resultEvent).toBeTruthy();
|
||||||
|
expect(resultEvent?.payload).toEqual(wrappedResult.toolResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
act,
|
||||||
|
} from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { describe, it, jest, beforeEach } from "@jest/globals";
|
import { describe, it, jest, beforeEach } from "@jest/globals";
|
||||||
import AppsTab from "../AppsTab";
|
import AppsTab from "../AppsTab";
|
||||||
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
import {
|
||||||
|
Tool,
|
||||||
|
CompatibilityCallToolResult,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Tabs } from "../ui/tabs";
|
import { Tabs } from "../ui/tabs";
|
||||||
|
|
||||||
// Mock AppRenderer component
|
// Mock AppRenderer component
|
||||||
|
|
@ -10,14 +19,19 @@ jest.mock("../AppRenderer", () => {
|
||||||
return function MockAppRenderer({
|
return function MockAppRenderer({
|
||||||
tool,
|
tool,
|
||||||
toolInput,
|
toolInput,
|
||||||
|
toolResult,
|
||||||
}: {
|
}: {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
toolInput?: Record<string, unknown>;
|
toolInput?: Record<string, unknown>;
|
||||||
|
toolResult?: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div data-testid="app-renderer">
|
<div data-testid="app-renderer">
|
||||||
<div>Tool: {tool.name}</div>
|
<div>Tool: {tool.name}</div>
|
||||||
<div data-testid="tool-input">{JSON.stringify(toolInput)}</div>
|
<div data-testid="tool-input">{JSON.stringify(toolInput)}</div>
|
||||||
|
<div data-testid="tool-result">
|
||||||
|
{JSON.stringify(toolResult ?? null)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -50,6 +64,12 @@ describe("AppsTab", () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
tools: [],
|
tools: [],
|
||||||
listTools: jest.fn(),
|
listTools: jest.fn(),
|
||||||
|
callTool: jest.fn(
|
||||||
|
async () =>
|
||||||
|
({
|
||||||
|
content: [],
|
||||||
|
}) as CompatibilityCallToolResult,
|
||||||
|
),
|
||||||
error: null,
|
error: null,
|
||||||
mcpClient: null,
|
mcpClient: null,
|
||||||
};
|
};
|
||||||
|
|
@ -131,7 +151,7 @@ describe("AppsTab", () => {
|
||||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open app renderer when an app card is clicked and Open App button is clicked if fields exist", () => {
|
it("should open app renderer when an app card is clicked and Open App button is clicked if fields exist", async () => {
|
||||||
const toolWithFields: Tool = {
|
const toolWithFields: Tool = {
|
||||||
name: "fieldsApp",
|
name: "fieldsApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -142,9 +162,16 @@ describe("AppsTab", () => {
|
||||||
},
|
},
|
||||||
_meta: { ui: { resourceUri: "ui://fields" } },
|
_meta: { ui: { resourceUri: "ui://fields" } },
|
||||||
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
const mockCallTool = jest.fn(
|
||||||
|
async () =>
|
||||||
|
({
|
||||||
|
content: [],
|
||||||
|
}) as CompatibilityCallToolResult,
|
||||||
|
);
|
||||||
|
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [toolWithFields],
|
tools: [toolWithFields],
|
||||||
|
callTool: mockCallTool,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCard = screen.getByText("fieldsApp").closest("div");
|
const appCard = screen.getByText("fieldsApp").closest("div");
|
||||||
|
|
@ -159,12 +186,19 @@ describe("AppsTab", () => {
|
||||||
const openAppButton = screen.getByRole("button", { name: /open app/i });
|
const openAppButton = screen.getByRole("button", { name: /open app/i });
|
||||||
fireEvent.click(openAppButton);
|
fireEvent.click(openAppButton);
|
||||||
|
|
||||||
// AppRenderer should be rendered
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(mockCallTool).toHaveBeenCalledWith("fieldsApp", {
|
||||||
|
field1: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
expect(screen.getByText("Tool: fieldsApp")).toBeInTheDocument();
|
expect(screen.getByText("Tool: fieldsApp")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should close app renderer when close button is clicked", () => {
|
it("should close app renderer when close button is clicked", async () => {
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -172,8 +206,9 @@ describe("AppsTab", () => {
|
||||||
// Open the app
|
// Open the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// Close the app (Deselect tool)
|
// Close the app (Deselect tool)
|
||||||
const closeButton = screen.getByRole("button", { name: /close app/i });
|
const closeButton = screen.getByRole("button", { name: /close app/i });
|
||||||
|
|
@ -204,7 +239,7 @@ describe("AppsTab", () => {
|
||||||
expect(screen.getByText("noDescriptionApp")).toBeInTheDocument();
|
expect(screen.getByText("noDescriptionApp")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset selected tool when tools list changes and selected tool is removed", () => {
|
it("should reset selected tool when tools list changes and selected tool is removed", async () => {
|
||||||
const { rerender } = renderAppsTab({
|
const { rerender } = renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -212,8 +247,9 @@ describe("AppsTab", () => {
|
||||||
// Select the app
|
// Select the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// Update tools list to remove the selected tool
|
// Update tools list to remove the selected tool
|
||||||
rerender(
|
rerender(
|
||||||
|
|
@ -226,7 +262,7 @@ describe("AppsTab", () => {
|
||||||
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain selected tool when tools list updates but includes the same tool", () => {
|
it("should maintain selected tool when tools list updates but includes the same tool", async () => {
|
||||||
const { rerender } = renderAppsTab({
|
const { rerender } = renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -234,8 +270,9 @@ describe("AppsTab", () => {
|
||||||
// Select the app
|
// Select the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// Update tools list with the same tool
|
// Update tools list with the same tool
|
||||||
rerender(
|
rerender(
|
||||||
|
|
@ -248,7 +285,7 @@ describe("AppsTab", () => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maximize and minimize the app window", () => {
|
it("should maximize and minimize the app window", async () => {
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -256,12 +293,12 @@ describe("AppsTab", () => {
|
||||||
// Select the app
|
// Select the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
|
||||||
|
|
||||||
// Now Maximize button should be visible
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("button", { name: /maximize/i }),
|
screen.getByRole("button", { name: /maximize/i }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// Initially, ListPane should be visible
|
// Initially, ListPane should be visible
|
||||||
expect(screen.getByText("MCP Apps")).toBeInTheDocument();
|
expect(screen.getByText("MCP Apps")).toBeInTheDocument();
|
||||||
|
|
@ -281,7 +318,7 @@ describe("AppsTab", () => {
|
||||||
expect(screen.getByText("MCP Apps")).toBeInTheDocument();
|
expect(screen.getByText("MCP Apps")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset maximized state when app is closed", () => {
|
it("should reset maximized state when app is closed", async () => {
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -289,7 +326,12 @@ describe("AppsTab", () => {
|
||||||
// Select the app
|
// Select the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /maximize/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
// Maximize
|
// Maximize
|
||||||
fireEvent.click(screen.getByRole("button", { name: /maximize/i }));
|
fireEvent.click(screen.getByRole("button", { name: /maximize/i }));
|
||||||
|
|
@ -311,7 +353,7 @@ describe("AppsTab", () => {
|
||||||
expect(appItem).toBeTruthy();
|
expect(appItem).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle empty resourceContentMap", () => {
|
it("should handle empty resourceContentMap", async () => {
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [mockAppTool],
|
tools: [mockAppTool],
|
||||||
});
|
});
|
||||||
|
|
@ -319,13 +361,13 @@ describe("AppsTab", () => {
|
||||||
// Open the app
|
// Open the app
|
||||||
const appCard = screen.getByText("weatherApp").closest("div");
|
const appCard = screen.getByText("weatherApp").closest("div");
|
||||||
fireEvent.click(appCard!);
|
fireEvent.click(appCard!);
|
||||||
// weatherApp has no properties in mockAppTool, so it should be open immediately
|
|
||||||
|
|
||||||
// AppRenderer should still be rendered
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle various input types and pass them to AppRenderer", () => {
|
it("should handle various input types and pass them to AppRenderer", async () => {
|
||||||
const toolWithComplexSchema: Tool = {
|
const toolWithComplexSchema: Tool = {
|
||||||
name: "complexApp",
|
name: "complexApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -359,6 +401,10 @@ describe("AppsTab", () => {
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
const toolInput = JSON.parse(
|
const toolInput = JSON.parse(
|
||||||
screen.getByTestId("tool-input").textContent || "{}",
|
screen.getByTestId("tool-input").textContent || "{}",
|
||||||
);
|
);
|
||||||
|
|
@ -367,7 +413,7 @@ describe("AppsTab", () => {
|
||||||
expect(toolInput.number).toBe(42);
|
expect(toolInput.number).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle nullable fields", () => {
|
it("should handle nullable fields", async () => {
|
||||||
const toolWithNullable: Tool = {
|
const toolWithNullable: Tool = {
|
||||||
name: "nullableApp",
|
name: "nullableApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -391,13 +437,17 @@ describe("AppsTab", () => {
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
const toolInput = JSON.parse(
|
const toolInput = JSON.parse(
|
||||||
screen.getByTestId("tool-input").textContent || "{}",
|
screen.getByTestId("tool-input").textContent || "{}",
|
||||||
);
|
);
|
||||||
expect(toolInput.nullableField).toBe(null);
|
expect(toolInput.nullableField).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow going back to input form from app renderer", () => {
|
it("should allow going back to input form from app renderer", async () => {
|
||||||
const toolWithFields: Tool = {
|
const toolWithFields: Tool = {
|
||||||
name: "fieldsApp",
|
name: "fieldsApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -416,13 +466,15 @@ describe("AppsTab", () => {
|
||||||
fireEvent.click(screen.getByText("fieldsApp"));
|
fireEvent.click(screen.getByText("fieldsApp"));
|
||||||
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
const backButton = screen.queryByRole("button", { name: /back to input/i });
|
const backButton = screen.queryByRole("button", { name: /back to input/i });
|
||||||
expect(backButton).toBeInTheDocument();
|
expect(backButton).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should skip input form if tool has no input fields", () => {
|
it("should skip input form if tool has no input fields", async () => {
|
||||||
const toolNoFields: Tool = {
|
const toolNoFields: Tool = {
|
||||||
name: "noFieldsApp",
|
name: "noFieldsApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -431,15 +483,27 @@ describe("AppsTab", () => {
|
||||||
},
|
},
|
||||||
_meta: { ui: { resourceUri: "ui://no-fields" } },
|
_meta: { ui: { resourceUri: "ui://no-fields" } },
|
||||||
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
const mockCallTool = jest.fn(
|
||||||
|
async () =>
|
||||||
|
({
|
||||||
|
content: [],
|
||||||
|
}) as CompatibilityCallToolResult,
|
||||||
|
);
|
||||||
|
|
||||||
renderAppsTab({
|
renderAppsTab({
|
||||||
tools: [toolNoFields],
|
tools: [toolNoFields],
|
||||||
|
callTool: mockCallTool,
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("noFieldsApp"));
|
fireEvent.click(screen.getByText("noFieldsApp"));
|
||||||
|
|
||||||
// Should see the renderer immediately
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
expect(mockCallTool).toHaveBeenCalledWith("noFieldsApp", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
expect(screen.getByText("Tool: noFieldsApp")).toBeInTheDocument();
|
expect(screen.getByText("Tool: noFieldsApp")).toBeInTheDocument();
|
||||||
|
|
||||||
// Should NOT see the back button
|
// Should NOT see the back button
|
||||||
|
|
@ -448,7 +512,7 @@ describe("AppsTab", () => {
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow going back to input form from app renderer if fields exist", () => {
|
it("should allow going back to input form from app renderer if fields exist", async () => {
|
||||||
const toolWithFields: Tool = {
|
const toolWithFields: Tool = {
|
||||||
name: "fieldsApp",
|
name: "fieldsApp",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -470,7 +534,9 @@ describe("AppsTab", () => {
|
||||||
expect(screen.getByText("App Input")).toBeInTheDocument();
|
expect(screen.getByText("App Input")).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
const backButton = screen.getByRole("button", { name: /back to input/i });
|
const backButton = screen.getByRole("button", { name: /back to input/i });
|
||||||
fireEvent.click(backButton);
|
fireEvent.click(backButton);
|
||||||
|
|
@ -478,4 +544,229 @@ describe("AppsTab", () => {
|
||||||
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("App Input")).toBeInTheDocument();
|
expect(screen.getByText("App Input")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should auto-render app from prefilled tools tab call without manual click", async () => {
|
||||||
|
const toolWithFields: Tool = {
|
||||||
|
name: "prefilledApp",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://prefilled" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
const onPrefilledToolCallConsumed = jest.fn();
|
||||||
|
const prefilledResult: CompatibilityCallToolResult = {
|
||||||
|
content: [{ type: "text", text: "weather result" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderAppsTab({
|
||||||
|
tools: [toolWithFields],
|
||||||
|
prefilledToolCall: {
|
||||||
|
id: 42,
|
||||||
|
toolName: "prefilledApp",
|
||||||
|
params: { city: "Lisbon" },
|
||||||
|
result: prefilledResult,
|
||||||
|
},
|
||||||
|
onPrefilledToolCallConsumed,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Tool: prefilledApp")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolInput = JSON.parse(
|
||||||
|
screen.getByTestId("tool-input").textContent || "{}",
|
||||||
|
);
|
||||||
|
expect(toolInput).toEqual({ city: "Lisbon" });
|
||||||
|
expect(screen.getByTestId("tool-result")).toHaveTextContent(
|
||||||
|
"weather result",
|
||||||
|
);
|
||||||
|
expect(onPrefilledToolCallConsumed).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not auto-render app when no prefilled tool call is provided", () => {
|
||||||
|
const toolWithFields: Tool = {
|
||||||
|
name: "manualApp",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://manual" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
|
||||||
|
renderAppsTab({
|
||||||
|
tools: [toolWithFields],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Select an app from the list to get started"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve submitted params while opening app", async () => {
|
||||||
|
const toolWithFields: Tool = {
|
||||||
|
name: "paramsApp",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://params" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
|
||||||
|
let resolveCall: ((result: CompatibilityCallToolResult) => void) | null =
|
||||||
|
null;
|
||||||
|
const mockCallTool = jest.fn(
|
||||||
|
() =>
|
||||||
|
new Promise<CompatibilityCallToolResult>((resolve) => {
|
||||||
|
resolveCall = resolve;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAppsTab({
|
||||||
|
tools: [toolWithFields],
|
||||||
|
callTool: mockCallTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("paramsApp"));
|
||||||
|
fireEvent.change(screen.getByLabelText("query"), {
|
||||||
|
target: { value: "first value" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("query"), {
|
||||||
|
target: { value: "second value" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveCall).toBeTruthy();
|
||||||
|
await act(async () => {
|
||||||
|
resolveCall?.({
|
||||||
|
content: [{ type: "text", text: "done" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("app-renderer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolInput = JSON.parse(
|
||||||
|
screen.getByTestId("tool-input").textContent || "{}",
|
||||||
|
);
|
||||||
|
expect(toolInput.query).toBe("first value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep input view when opening fails and recover button state", async () => {
|
||||||
|
const toolWithFields: Tool = {
|
||||||
|
name: "failingApp",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://failing" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
|
||||||
|
const mockCallTool = jest.fn(
|
||||||
|
async () =>
|
||||||
|
await Promise.reject<CompatibilityCallToolResult>(
|
||||||
|
new Error("tool failed"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAppsTab({
|
||||||
|
tools: [toolWithFields],
|
||||||
|
callTool: mockCallTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("failingApp"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /open app/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCallTool).toHaveBeenCalledWith("failingApp", {
|
||||||
|
query: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const openAppButton = screen.getByRole("button", { name: /open app/i });
|
||||||
|
expect(openAppButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("App Input")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("app-renderer")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore stale results from older app launches", async () => {
|
||||||
|
const appA: Tool = {
|
||||||
|
name: "appA",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://app-a" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
const appB: Tool = {
|
||||||
|
name: "appB",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
_meta: { ui: { resourceUri: "ui://app-b" } },
|
||||||
|
} as Tool & { _meta?: { ui?: { resourceUri?: string } } };
|
||||||
|
|
||||||
|
const pendingCalls: {
|
||||||
|
name: string;
|
||||||
|
resolve: (result: CompatibilityCallToolResult) => void;
|
||||||
|
}[] = [];
|
||||||
|
const mockCallTool = jest.fn(
|
||||||
|
(name: string) =>
|
||||||
|
new Promise<CompatibilityCallToolResult>((resolve) => {
|
||||||
|
pendingCalls.push({ name, resolve });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAppsTab({
|
||||||
|
tools: [appA, appB],
|
||||||
|
callTool: mockCallTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("appA"));
|
||||||
|
fireEvent.click(screen.getByText("appB"));
|
||||||
|
|
||||||
|
const appBCall = pendingCalls.find((call) => call.name === "appB");
|
||||||
|
const appACall = pendingCalls.find((call) => call.name === "appA");
|
||||||
|
expect(appBCall).toBeTruthy();
|
||||||
|
expect(appACall).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
appBCall?.resolve({
|
||||||
|
content: [{ type: "text", text: "result from appB" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Tool: appB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("tool-result")).toHaveTextContent("appB");
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
appACall?.resolve({
|
||||||
|
content: [{ type: "text", text: "result from appA" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Tool: appB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("tool-result")).toHaveTextContent("appB");
|
||||||
|
expect(screen.getByTestId("tool-result")).not.toHaveTextContent("appA");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ describe("AuthDebugger", () => {
|
||||||
const updateAuthState = jest.fn();
|
const updateAuthState = jest.fn();
|
||||||
const mockResourceMetadata = {
|
const mockResourceMetadata = {
|
||||||
resource: "https://example.com/mcp",
|
resource: "https://example.com/mcp",
|
||||||
authorization_servers: ["https://custom-auth.example.com"],
|
authorization_servers: ["https://custom-auth.example.com/mcp/tenant"],
|
||||||
bearer_methods_supported: ["header", "body"],
|
bearer_methods_supported: ["header", "body"],
|
||||||
resource_documentation: "https://example.com/mcp/docs",
|
resource_documentation: "https://example.com/mcp/docs",
|
||||||
resource_policy_uri: "https://example.com/mcp/policy",
|
resource_policy_uri: "https://example.com/mcp/policy",
|
||||||
|
|
@ -733,11 +733,17 @@ describe("AuthDebugger", () => {
|
||||||
expect(updateAuthState).toHaveBeenCalledWith(
|
expect(updateAuthState).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
resourceMetadata: mockResourceMetadata,
|
resourceMetadata: mockResourceMetadata,
|
||||||
authServerUrl: new URL("https://custom-auth.example.com"),
|
authServerUrl: new URL(
|
||||||
|
"https://custom-auth.example.com/mcp/tenant",
|
||||||
|
),
|
||||||
oauthStep: "client_registration",
|
oauthStep: "client_registration",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||||
|
new URL("https://custom-auth.example.com/mcp/tenant"),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle protected resource metadata fetch failure gracefully", async () => {
|
it("should handle protected resource metadata fetch failure gracefully", async () => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
generateOAuthErrorDescription,
|
generateOAuthErrorDescription,
|
||||||
parseOAuthCallbackParams,
|
parseOAuthCallbackParams,
|
||||||
generateOAuthState,
|
generateOAuthState,
|
||||||
|
getAuthorizationServerMetadataDiscoveryUrl,
|
||||||
} from "@/utils/oauthUtils.ts";
|
} from "@/utils/oauthUtils.ts";
|
||||||
|
|
||||||
describe("parseOAuthCallbackParams", () => {
|
describe("parseOAuthCallbackParams", () => {
|
||||||
|
|
@ -84,3 +85,29 @@ describe("generateOAuthErrorDescription", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getAuthorizationServerMetadataDiscoveryUrl", () => {
|
||||||
|
it("uses root discovery URL for root authorization server URL", () => {
|
||||||
|
expect(
|
||||||
|
getAuthorizationServerMetadataDiscoveryUrl("https://example.com"),
|
||||||
|
).toBe("https://example.com/.well-known/oauth-authorization-server");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inserts tenant path for non-root authorization server URL", () => {
|
||||||
|
expect(
|
||||||
|
getAuthorizationServerMetadataDiscoveryUrl("https://example.com/tenant1"),
|
||||||
|
).toBe(
|
||||||
|
"https://example.com/.well-known/oauth-authorization-server/tenant1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips trailing slash before appending tenant path", () => {
|
||||||
|
expect(
|
||||||
|
getAuthorizationServerMetadataDiscoveryUrl(
|
||||||
|
"https://example.com/tenant1/",
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
"https://example.com/.well-known/oauth-authorization-server/tenant1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,31 @@ export const generateOAuthErrorDescription = (
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the primary OAuth authorization server metadata discovery URL
|
||||||
|
* for a given authorization server URL, including tenant path handling.
|
||||||
|
*/
|
||||||
|
export const getAuthorizationServerMetadataDiscoveryUrl = (
|
||||||
|
authorizationServerUrl: string | URL,
|
||||||
|
): string => {
|
||||||
|
const url =
|
||||||
|
typeof authorizationServerUrl === "string"
|
||||||
|
? new URL(authorizationServerUrl)
|
||||||
|
: authorizationServerUrl;
|
||||||
|
const hasPath = url.pathname !== "/";
|
||||||
|
|
||||||
|
if (!hasPath) {
|
||||||
|
return new URL("/.well-known/oauth-authorization-server", url.origin).href;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip trailing slash to avoid double slashes in tenant-aware discovery URLs.
|
||||||
|
const pathname = url.pathname.endsWith("/")
|
||||||
|
? url.pathname.slice(0, -1)
|
||||||
|
: url.pathname;
|
||||||
|
|
||||||
|
return new URL(
|
||||||
|
`/.well-known/oauth-authorization-server${pathname}`,
|
||||||
|
url.origin,
|
||||||
|
).href;
|
||||||
|
};
|
||||||
|
|
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -5146,9 +5146,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
|
@ -7573,9 +7573,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.11.7",
|
"version": "4.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz",
|
||||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
"integrity": "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
|
|
@ -11010,9 +11010,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.1",
|
"version": "6.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
|
|
@ -14027,24 +14027,6 @@
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"server/node_modules/express-rate-limit": {
|
|
||||||
"version": "8.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
|
||||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ip-address": "10.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/express-rate-limit"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"express": ">= 4.11"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
216
server/LICENSE
Normal file
216
server/LICENSE
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
||||||
|
|
||||||
|
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
|
||||||
|
|
||||||
|
No rights beyond those granted by the applicable original license are conveyed for such contributions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright
|
||||||
|
owner or by an individual or Legal Entity authorized to submit on behalf
|
||||||
|
of the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International (CC-BY-4.0)
|
||||||
|
|
||||||
|
Documentation in this project (excluding specifications) is licensed under
|
||||||
|
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
||||||
|
the full license text.
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
"mcp-inspector-server": "build/index.js"
|
"mcp-inspector-server": "build/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"build"
|
"build",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && shx cp -R static build",
|
"build": "tsc && shx cp -R static build",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue