Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d73f99efbf |
||
|
|
1ec570c27d |
||
|
|
f41b666edf |
||
|
|
c3d8e43d1f |
||
|
|
abbbddb2e3 |
||
|
|
649af5856e |
||
|
|
242751e8ee |
||
|
|
ba167cef5b |
||
|
|
48723c95dc |
||
|
|
96c49c7364 |
||
|
|
cce08de417 |
||
|
|
2e3e4c7ab7 |
||
|
|
3aa19f5574 |
||
|
|
579c3903f3 |
||
|
|
a6cdbf4dea |
||
|
|
ebaccfd623 |
||
|
|
cda92bdaac |
||
|
|
d6402efee6 |
||
|
|
40a8de87ed |
||
|
|
11845d51cb |
||
|
|
cf05b37694 |
||
|
|
642a9112e7 |
||
|
|
18ce197639 |
||
|
|
562feeb281 |
||
|
|
71e3cfbd1e |
||
|
|
781a93db14 |
||
|
|
8102fbe27a |
||
|
|
599dafc105 |
||
|
|
d811ba11f1 | ||
|
|
53dbe83b7a | ||
|
|
76d64c822f |
||
|
|
1cd1096d29 |
||
|
|
d8cff7f016 |
||
|
|
7112282ce2 |
||
|
|
76639ead0c |
||
|
|
3bcfedd8e4 |
||
|
|
fd8248e56b |
||
|
|
ced1408d3a |
||
|
|
20b5783d3b |
||
|
|
4852bb3fc4 |
||
|
|
10962d3114 |
||
|
|
7bdcdb6a85 |
||
|
|
fab6abed58 |
||
|
|
2e15194d4e |
||
|
|
42a1d825b1 |
||
|
|
40ce2f784e |
||
|
|
31bd12967f |
||
|
|
86b9438e0e |
||
|
|
63df72cb15 | ||
|
|
d31124c982 |
||
|
|
97b70ee252 |
||
|
|
0e30ad18c7 |
||
|
|
f7221c4d02 |
||
|
|
55d256293f |
||
|
|
7097923966 |
||
|
|
5850690363 | ||
|
|
3b9d38fbac | ||
|
|
752985790e | ||
|
|
18f09adae3 | ||
|
|
06a8169e6f | ||
|
|
a714b63b7f | ||
|
|
f2450d7fa4 | ||
|
|
9e9dfb95b5 | ||
|
|
1c0fdc3063 | ||
|
|
f4c86ff964 | ||
|
|
dab3489b73 | ||
|
|
4307b95aee | ||
|
|
1a4ecb7575 | ||
|
|
a72e93e503 | ||
|
|
b2a94a21a5 |
||
|
|
aa8d20609c | ||
|
|
7b1170d1da |
||
|
|
a96189b2af |
||
|
|
0588ec098e | ||
|
|
275175cda1 |
||
|
|
25e5f3a6e8 |
||
|
|
f5054df1fb |
||
|
|
3a32d1024b |
||
|
|
040b5a96db |
||
|
|
67a71f1b60 | ||
|
|
e73b9a2f9a |
62 changed files with 5348 additions and 3267 deletions
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
44
.github/workflows/python.yml
vendored
44
.github/workflows/python.yml
vendored
|
|
@ -2,11 +2,13 @@ name: Python
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
# Superseded runs on the same ref are cancelled (e.g. rapid amend-pushes),
|
||||
# except on main, where every merge keeps its own CI run
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
detect-packages:
|
||||
|
|
@ -34,10 +36,10 @@ jobs:
|
|||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version-file: "src/${{ matrix.package }}/.python-version"
|
||||
|
||||
|
|
@ -71,10 +73,10 @@ jobs:
|
|||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version-file: "src/${{ matrix.package }}/.python-version"
|
||||
|
||||
|
|
@ -91,31 +93,7 @@ jobs:
|
|||
run: uv build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist-${{ matrix.package }}
|
||||
path: src/${{ matrix.package }}/dist/
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, detect-packages]
|
||||
if: github.event_name == 'release'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
|
||||
name: Publish ${{ matrix.package }}
|
||||
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write # Required for trusted publishing
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist-${{ matrix.package }}
|
||||
path: dist/
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
|
|
|||
7
.github/workflows/readme-pr-check.yml
vendored
7
.github/workflows/readme-pr-check.yml
vendored
|
|
@ -1,7 +1,10 @@
|
|||
name: README PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
# pull_request_target, not pull_request: fork PRs get a read-only token under
|
||||
# pull_request, so the label and comment calls fail with 403. This job only
|
||||
# calls the API and never checks out PR code, so the write token is safe.
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
paths:
|
||||
- 'README.md'
|
||||
|
|
@ -10,7 +13,7 @@ on:
|
|||
|
||||
jobs:
|
||||
check-readme-only:
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -1,9 +1,7 @@
|
|||
name: Automatic Release Creation
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
|
||||
jobs:
|
||||
create-metadata:
|
||||
|
|
@ -27,7 +25,7 @@ jobs:
|
|||
echo "Using last release hash: ${HASH}"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Create version name
|
||||
id: create-version
|
||||
|
|
@ -43,7 +41,7 @@ jobs:
|
|||
cat RELEASE_NOTES.md
|
||||
|
||||
- name: Release notes
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-notes
|
||||
path: RELEASE_NOTES.md
|
||||
|
|
@ -79,7 +77,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Update packages
|
||||
run: |
|
||||
|
|
@ -123,10 +121,10 @@ jobs:
|
|||
ref: ${{ needs.create-metadata.outputs.version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version-file: "src/${{ matrix.package }}/.python-version"
|
||||
|
||||
|
|
@ -138,6 +136,15 @@ jobs:
|
|||
working-directory: src/${{ matrix.package }}
|
||||
run: uv run --frozen pyright
|
||||
|
||||
- name: Run tests
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: |
|
||||
if [ -d "tests" ] || [ -d "test" ]; then
|
||||
uv run --frozen pytest
|
||||
else
|
||||
echo "No tests directory, skipping"
|
||||
fi
|
||||
|
||||
- name: Build package
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: uv build
|
||||
|
|
@ -146,6 +153,7 @@ jobs:
|
|||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: src/${{ matrix.package }}/dist
|
||||
skip-existing: true # re-runs tolerate already-uploaded files
|
||||
|
||||
publish-npm:
|
||||
needs: [update-packages, create-metadata]
|
||||
|
|
@ -156,18 +164,25 @@ jobs:
|
|||
package: ${{ fromJson(needs.create-metadata.outputs.npm_packages) }}
|
||||
name: Build ${{ matrix.package }}
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for npm trusted publishing (OIDC)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.create-metadata.outputs.version }}
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# OIDC trusted publishing requires npm >=11.5.1; Node 22's bundled npm is 10.x.
|
||||
- name: Ensure npm CLI supports OIDC trusted publishing
|
||||
run: npm install -g npm@^11.5.1
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm ci
|
||||
|
|
@ -182,16 +197,22 @@ jobs:
|
|||
fi
|
||||
echo "Version $VERSION is new, proceeding with publish"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm test --if-present
|
||||
|
||||
- name: Build package
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm run build
|
||||
|
||||
# Authenticates via OIDC trusted publishing (no token) — each package's
|
||||
# npm trusted publisher is bound to this workflow + the release environment
|
||||
- name: Publish package
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: |
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
|
||||
create-release:
|
||||
needs: [update-packages, create-metadata, publish-pypi, publish-npm]
|
||||
|
|
@ -207,7 +228,7 @@ jobs:
|
|||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download release notes
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-notes
|
||||
|
||||
|
|
|
|||
46
.github/workflows/typescript.yml
vendored
46
.github/workflows/typescript.yml
vendored
|
|
@ -2,11 +2,13 @@ name: TypeScript
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
# Superseded runs on the same ref are cancelled (e.g. rapid amend-pushes),
|
||||
# except on main, where every merge keeps its own CI run
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
detect-packages:
|
||||
|
|
@ -32,7 +34,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
|
@ -55,7 +57,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
|
@ -68,35 +70,3 @@ jobs:
|
|||
working-directory: src/${{ matrix.package }}
|
||||
run: npm run build
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, detect-packages]
|
||||
if: github.event_name == 'release'
|
||||
environment: release
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
|
||||
name: Publish ${{ matrix.package }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm ci
|
||||
|
||||
- name: Publish package
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ These servers aim to demonstrate MCP features and the official SDKs.
|
|||
The following reference servers are now archived and can be found at [servers-archived](https://github.com/modelcontextprotocol/servers-archived).
|
||||
|
||||
- **[AWS KB Retrieval](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/aws-kb-retrieval-server)** - Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
|
||||
- **[Brave Search](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search)** - Web and local search using Brave's Search API. Has been replaced by the [official server](https://github.com/brave/brave-search-mcp-server).
|
||||
- **[Brave Search](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search)** - Web and local search using Brave's Search API. Has been replaced by the [official server](https://github.com/brave/brave-search-mcp-server) ([`@brave/brave-search-mcp-server`](https://www.npmjs.com/package/@brave/brave-search-mcp-server)).
|
||||
- **[EverArt](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/everart)** - AI image generation using various models.
|
||||
- **[GitHub](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/github)** - Repository management, file operations, and GitHub API integration.
|
||||
- **[GitLab](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/gitlab)** - GitLab API, enabling project management.
|
||||
|
|
@ -145,6 +145,10 @@ See [ADDITIONAL.md](ADDITIONAL.md) for a curated list of frameworks and resource
|
|||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for information about contributing to this repository.
|
||||
|
||||
## 📦 Releasing
|
||||
|
||||
See [RELEASING.md](RELEASING.md) for how packages are published (OIDC trusted publishing from CI — no registry tokens) and how to retry a failed publish.
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities.
|
||||
|
|
|
|||
49
RELEASING.md
Normal file
49
RELEASING.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Releasing
|
||||
|
||||
How the packages in this repository are published, and what to do when a publish fails.
|
||||
|
||||
## How publishing works
|
||||
|
||||
All packages publish exclusively from the [`release.yml`](.github/workflows/release.yml) GitHub Actions workflow, gated by the `release` environment (a required reviewer must approve each deployment). Releases are triggered deliberately by a maintainer via **workflow_dispatch** (Actions → Release → Run workflow, or `gh workflow run release.yml`) — there is no scheduled/automatic release.
|
||||
|
||||
**Authentication is OIDC trusted publishing on both registries — there are no registry tokens.**
|
||||
|
||||
- **npm** (TypeScript servers): each `@modelcontextprotocol/*` package is registered on npmjs.com with a [trusted publisher](https://docs.npmjs.com/trusted-publishers) bound to this repository, workflow filename `release.yml`, and environment `release` (the binding is case-sensitive). Packages publish with [provenance attestations](https://docs.npmjs.com/generating-provenance-statements).
|
||||
- **PyPI** (Python servers): published via [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) using `pypa/gh-action-pypi-publish`.
|
||||
|
||||
A release run:
|
||||
|
||||
1. **Detects changed packages** since the last release tag — a package counts as changed if any `.py`, `.ts`, or `.md` file in its directory changed (READMEs ship inside the published artifacts).
|
||||
2. **Stamps versions and pushes the release tag** — versions are date-based (CalVer, e.g. `2026.7.4`).
|
||||
3. **Publishes each changed package as an independent matrix job** (`fail-fast: false` — one package's failure never blocks another). Each job: checkout at the release tag → install → double-publish guard → **run the package's tests** (plus `pyright` for Python) → build → publish. The guard differs by registry: the npm job aborts before tests if the version already exists; for PyPI the skip happens at the publish step itself (`skip-existing` on the upload action).
|
||||
4. **Creates the GitHub release** with generated notes.
|
||||
|
||||
## When a publish fails
|
||||
|
||||
A failed matrix leg means that one package didn't publish; everything that succeeded stays published.
|
||||
|
||||
**Preferred: re-run the failed jobs on the same run.**
|
||||
|
||||
```bash
|
||||
gh run rerun <run-id> --failed --repo modelcontextprotocol/servers
|
||||
```
|
||||
|
||||
- A re-run is still a `release.yml` run in the `release` environment, so it satisfies the trusted-publisher binding.
|
||||
- It re-runs only the failed legs, checked out at the original release tag — it publishes exactly the tagged code, and the double-publish guard keeps already-published packages safe.
|
||||
- It needs a fresh `release` environment approval, and the run must be complete first (approve or reject any pending deployments).
|
||||
- GitHub's re-run window is ~30 days from the original run, and re-runs execute the *original* workflow snapshot — workflow fixes on `main` don't apply to a re-run.
|
||||
|
||||
**Otherwise: let the next release pick it up.** If the re-run window has closed (or the fix required a workflow change), the failed version simply never exists on that registry — that's benign; npm and PyPI version histories don't need to match. The package publishes at the next version, provided it has a qualifying change (`.py`, `.ts`, or `.md`) since the last release tag.
|
||||
|
||||
**Never:**
|
||||
|
||||
- Publish manually with an npm token or from a laptop — there are no registry tokens, and manual publishes would break the provenance/trust chain.
|
||||
- Dispatch a fresh `release.yml` run expecting it to retry a failed version — versions are date-granular, so a same-day dispatch collides with the existing tag, and a later dispatch mints a *new* version. Neither retries the failed one.
|
||||
|
||||
## Environment approvals
|
||||
|
||||
The `release` environment's required-reviewer list is configured in the repository settings (Settings → Environments → `release`). Reviewer rights come only from that list — repository admin does not confer deployment approval.
|
||||
|
||||
---
|
||||
|
||||
Planned changes to this process — semver via changesets for the TypeScript packages, publishing triggered by manually-created GitHub Releases — are tracked in [#4463](https://github.com/modelcontextprotocol/servers/issues/4463). This document will be updated when that work merges.
|
||||
3754
package-lock.json
generated
3754
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,5 +23,9 @@
|
|||
"@modelcontextprotocol/server-memory": "*",
|
||||
"@modelcontextprotocol/server-filesystem": "*",
|
||||
"@modelcontextprotocol/server-sequential-thinking": "*"
|
||||
},
|
||||
"overrides": {
|
||||
"qs": ">=6.15.2",
|
||||
"hono": ">=4.12.21"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ def has_changes(path: Path, git_hash: GitHash) -> bool:
|
|||
)
|
||||
|
||||
changed_files = [Path(f) for f in output.stdout.splitlines()]
|
||||
relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts"]]
|
||||
# .md counts as a change: READMEs ship inside the published package
|
||||
# (npm tarball / PyPI long_description)
|
||||
relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts", ".md"]]
|
||||
return len(relevant_files) >= 1
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ npm run start:streamableHttp
|
|||
### Install
|
||||
```shell
|
||||
npm install -g @modelcontextprotocol/server-everything@latest
|
||||
````
|
||||
```
|
||||
|
||||
### Run the default (stdio) server
|
||||
```shell
|
||||
|
|
@ -134,3 +134,7 @@ npx @modelcontextprotocol/server-everything sse
|
|||
```shell
|
||||
npx @modelcontextprotocol/server-everything streamableHttp
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ describe('Registration Index Files', () => {
|
|||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({
|
||||
roots: {},
|
||||
elicitation: {},
|
||||
elicitation: { url: {} },
|
||||
sampling: {},
|
||||
})),
|
||||
},
|
||||
|
|
@ -67,14 +67,17 @@ describe('Registration Index Files', () => {
|
|||
|
||||
registerConditionalTools(mockServerWithCapabilities);
|
||||
|
||||
// Should register 3 conditional tools + 3 task-based tools when all capabilities present
|
||||
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(3);
|
||||
// Should register 4 conditional tools via registerTool when all capabilities
|
||||
// are present. Task-based tools register via registerToolTask (counted separately),
|
||||
// so they are not included in this registerTool count.
|
||||
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4);
|
||||
|
||||
const registeredTools = (
|
||||
mockServerWithCapabilities.registerTool as any
|
||||
).mock.calls.map((call: any[]) => call[0]);
|
||||
expect(registeredTools).toContain('get-roots-list');
|
||||
expect(registeredTools).toContain('trigger-elicitation-request');
|
||||
expect(registeredTools).toContain('trigger-url-elicitation');
|
||||
expect(registeredTools).toContain('trigger-sampling-request');
|
||||
|
||||
// Task-based tools are registered via experimental.tasks.registerToolTask
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { registerEchoTool, EchoSchema } from '../tools/echo.js';
|
||||
import { registerGetSumTool } from '../tools/get-sum.js';
|
||||
|
|
@ -13,8 +13,13 @@ import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-lo
|
|||
import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js';
|
||||
import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js';
|
||||
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
|
||||
import {
|
||||
registerTriggerUrlElicitationTool,
|
||||
__resetIssuedErrorPathElicitations,
|
||||
} from '../tools/trigger-url-elicitation.js';
|
||||
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
|
||||
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
|
||||
import { registerSimulateResearchQueryTool } from '../tools/simulate-research-query.js';
|
||||
|
||||
// Helper to capture registered tool handlers
|
||||
function createMockServer() {
|
||||
|
|
@ -706,6 +711,318 @@ describe('Tools', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('trigger-url-elicitation', () => {
|
||||
// The error-path marker is module-level state shared across cases; reset it
|
||||
// so tests are independent of order and of each other's leftover keys.
|
||||
beforeEach(() => {
|
||||
__resetIssuedErrorPathElicitations();
|
||||
});
|
||||
|
||||
it('should not register when client does not support URL elicitation', () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register when client supports URL elicitation', () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
expect(mockServer.registerTool).toHaveBeenCalledWith(
|
||||
'trigger-url-elicitation',
|
||||
expect.objectContaining({
|
||||
title: 'Trigger URL Elicitation Tool',
|
||||
description: expect.stringContaining('URL elicitation'),
|
||||
}),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should send URL-mode elicitation request when errorPath is false', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({
|
||||
action: 'accept',
|
||||
});
|
||||
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
const result = await handler(
|
||||
{
|
||||
url: 'https://example.com/verify',
|
||||
message: 'Open this page to verify your identity',
|
||||
elicitationId: 'elicitation-123',
|
||||
errorPath: false,
|
||||
},
|
||||
{ sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
expect(mockSendRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'elicitation/create',
|
||||
params: expect.objectContaining({
|
||||
mode: 'url',
|
||||
url: 'https://example.com/verify',
|
||||
message: 'Open this page to verify your identity',
|
||||
elicitationId: 'elicitation-123',
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain(
|
||||
'✅ User completed the URL elicitation flow.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not register when client has no elicitation capability at all', () => {
|
||||
const mockServer = {
|
||||
registerTool: vi.fn(),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({})),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not register when client capabilities are undefined', () => {
|
||||
const mockServer = {
|
||||
registerTool: vi.fn(),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => undefined),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default the elicitationId to a random UUID when omitted', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({
|
||||
action: 'accept',
|
||||
});
|
||||
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
await handler(
|
||||
{
|
||||
url: 'https://example.com/verify',
|
||||
message: 'Open this page to verify your identity',
|
||||
errorPath: false,
|
||||
},
|
||||
{ sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
const sentParams = mockSendRequest.mock.calls[0][0].params;
|
||||
expect(sentParams.elicitationId).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||
);
|
||||
});
|
||||
|
||||
it('should report a declined URL elicitation', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' });
|
||||
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
const result = await handler(
|
||||
{
|
||||
url: 'https://example.com/verify',
|
||||
message: 'Open this page to verify your identity',
|
||||
elicitationId: 'elicitation-123',
|
||||
errorPath: false,
|
||||
},
|
||||
{ sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain('❌ User declined to open the URL');
|
||||
});
|
||||
|
||||
it('should report a cancelled URL elicitation', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' });
|
||||
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
const result = await handler(
|
||||
{
|
||||
url: 'https://example.com/verify',
|
||||
message: 'Open this page to verify your identity',
|
||||
elicitationId: 'elicitation-123',
|
||||
errorPath: false,
|
||||
},
|
||||
{ sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain(
|
||||
'⚠️ User cancelled the URL elicitation'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
|
||||
expect.assertions(5);
|
||||
|
||||
try {
|
||||
await handler(
|
||||
{
|
||||
url: 'https://example.com/connect',
|
||||
message: 'Authorization is required to continue.',
|
||||
elicitationId: 'elicitation-xyz',
|
||||
errorPath: true,
|
||||
},
|
||||
{}
|
||||
);
|
||||
} catch (error: any) {
|
||||
expect(error.code).toBe(-32042);
|
||||
const prerequisite = error.data.elicitations[0];
|
||||
expect(prerequisite.mode).toBe('url');
|
||||
// The prerequisite must NOT reuse the failing URL, otherwise the client
|
||||
// would complete it, retry, hit the same error, and loop forever.
|
||||
expect(prerequisite.url).toBe('https://modelcontextprotocol.io');
|
||||
expect(prerequisite.url).not.toBe('https://example.com/connect');
|
||||
// It carries its own elicitation id for the prerequisite itself.
|
||||
expect(typeof prerequisite.elicitationId).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => {
|
||||
const handlers: Map<string, Function> = new Map();
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' });
|
||||
|
||||
const mockServer = {
|
||||
registerTool: vi.fn((name: string, config: any, handler: Function) => {
|
||||
handlers.set(name, handler);
|
||||
}),
|
||||
server: {
|
||||
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
||||
},
|
||||
} as unknown as McpServer;
|
||||
|
||||
registerTriggerUrlElicitationTool(mockServer);
|
||||
|
||||
const handler = handlers.get('trigger-url-elicitation')!;
|
||||
// A real client retries with the *same* arguments and does not echo the
|
||||
// prerequisite's elicitationId. Note these args omit elicitationId, so the
|
||||
// correlation must rely on stable inputs (session + url), not a per-call
|
||||
// random id.
|
||||
const args = {
|
||||
url: 'https://example.com/connect',
|
||||
message: 'Authorization is required to continue.',
|
||||
errorPath: true,
|
||||
};
|
||||
const extra = { sessionId: 'session-1', sendRequest: mockSendRequest };
|
||||
|
||||
// First call: error path issues the prerequisite and throws -32042.
|
||||
let prerequisiteUrl: string | undefined;
|
||||
try {
|
||||
await handler(args, extra);
|
||||
throw new Error('expected first call to throw');
|
||||
} catch (error: any) {
|
||||
expect(error.code).toBe(-32042);
|
||||
prerequisiteUrl = error.data.elicitations[0].url;
|
||||
expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io');
|
||||
expect(mockSendRequest).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
// Plain retry with identical arguments: errorPath is ignored and the call
|
||||
// proceeds via the request path instead of throwing the prerequisite again.
|
||||
const result = await handler({ ...args }, extra);
|
||||
|
||||
expect(mockSendRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'elicitation/create',
|
||||
params: expect.objectContaining({
|
||||
mode: 'url',
|
||||
url: 'https://example.com/connect',
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(result.content[0].text).toContain(
|
||||
'✅ User completed the URL elicitation flow.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get-roots-list', () => {
|
||||
it('should not register when client does not support roots', () => {
|
||||
const { mockServer } = createMockServer();
|
||||
|
|
@ -738,6 +1055,90 @@ describe('Tools', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('simulate-research-query', () => {
|
||||
function createMockServerWithTasks() {
|
||||
const taskHandlers: Record<string, any> = {};
|
||||
const mockServer = {
|
||||
experimental: {
|
||||
tasks: {
|
||||
registerToolTask: vi.fn((_name: string, _config: any, handler: any) => {
|
||||
Object.assign(taskHandlers, handler);
|
||||
}),
|
||||
},
|
||||
},
|
||||
server: { getClientCapabilities: vi.fn(() => ({ elicitation: {} })) },
|
||||
} as unknown as McpServer;
|
||||
return { mockServer, taskHandlers };
|
||||
}
|
||||
|
||||
function createMockTaskStore(taskId: string) {
|
||||
return {
|
||||
createTask: vi.fn().mockResolvedValue({
|
||||
taskId,
|
||||
status: 'working',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
ttl: 300000,
|
||||
pollInterval: 1000,
|
||||
}),
|
||||
updateTaskStatus: vi.fn().mockResolvedValue(undefined),
|
||||
storeTaskResult: vi.fn().mockResolvedValue(undefined),
|
||||
getTask: vi.fn(),
|
||||
getTaskResult: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
it('should pass relatedTask to sendRequest when elicitation is triggered', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { mockServer, taskHandlers } = createMockServerWithTasks();
|
||||
registerSimulateResearchQueryTool(mockServer);
|
||||
|
||||
const mockTaskStore = createMockTaskStore('task-abc');
|
||||
const mockSendRequest = vi.fn().mockResolvedValue({
|
||||
action: 'accept',
|
||||
content: { interpretation: 'technical' },
|
||||
});
|
||||
|
||||
await taskHandlers.createTask(
|
||||
{ topic: 'python', ambiguous: true },
|
||||
{ taskStore: mockTaskStore, sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(mockSendRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'elicitation/create' }),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ relatedTask: { taskId: 'task-abc' } })
|
||||
);
|
||||
});
|
||||
|
||||
it('should complete without elicitation for non-ambiguous query', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { mockServer, taskHandlers } = createMockServerWithTasks();
|
||||
registerSimulateResearchQueryTool(mockServer);
|
||||
|
||||
const mockTaskStore = createMockTaskStore('task-def');
|
||||
const mockSendRequest = vi.fn();
|
||||
|
||||
await taskHandlers.createTask(
|
||||
{ topic: 'python', ambiguous: false },
|
||||
{ taskStore: mockTaskStore, sendRequest: mockSendRequest }
|
||||
);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(mockSendRequest).not.toHaveBeenCalled();
|
||||
expect(mockTaskStore.storeTaskResult).toHaveBeenCalledWith(
|
||||
'task-def', 'completed', expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gzip-file-as-resource', () => {
|
||||
it('should compress data URI and return resource link', async () => {
|
||||
const registeredResources: any[] = [];
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity).
|
||||
- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs.
|
||||
- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after.
|
||||
- `trigger-long-running-operation` (tools/trigger-trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client.
|
||||
- `trigger-long-running-operation` (tools/trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client.
|
||||
- `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level.
|
||||
- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to.
|
||||
- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content.
|
||||
- `trigger-url-elicitation` (tools/trigger-url-elicitation.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, or throws MCP error `-32042` (`UrlElicitationRequiredError`) when `errorPath=true`. On the error path the prerequisite elicitation it returns points at a different URL than the failing request (`https://modelcontextprotocol.io`); when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path, so the client does not loop on the same error. The retry marker is one-shot per `(session, url, elicitationId)`: it is cleared on the recognized retry, so re-running the error path with identical arguments without an intervening prerequisite is treated as a retry and proceeds. Requires client capability `elicitation.url`.
|
||||
- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload.
|
||||
- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing.
|
||||
- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`.
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
- `prompts: {}`
|
||||
- `resources: { subscribe: true }`
|
||||
- **Server Instructions**
|
||||
- Loaded from the docs folder (`server-instructions.md`).
|
||||
- Loaded from the docs folder (`instructions.md`).
|
||||
- **Registrations**
|
||||
- Registers **tools** via `registerTools(server)`.
|
||||
- Registers **resources** via `registerResources(server)`.
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ src/everything
|
|||
│ ├── trigger-elicitation-request-async.ts
|
||||
│ ├── trigger-long-running-operation.ts
|
||||
│ ├── trigger-sampling-request.ts
|
||||
│ └── trigger-sampling-request-async.ts
|
||||
│ ├── trigger-sampling-request-async.ts
|
||||
│ └── trigger-url-elicitation.ts
|
||||
└── transports
|
||||
├── sse.ts
|
||||
├── stdio.ts
|
||||
|
|
@ -84,9 +85,19 @@ src/everything
|
|||
### `docs/`
|
||||
|
||||
- `architecture.md`
|
||||
- Describes the server architecture, including transport layers, session initialization, and primitive registration.
|
||||
- `extension.md`
|
||||
- Explains how to extend the server with new tools, prompts, resources, or transports.
|
||||
- `features.md`
|
||||
- A comprehensive reference listing all implemented prompts, resources, tools, and protocol behaviors.
|
||||
- `how-it-works.md`
|
||||
- Explains how SSE and Streamable HTTP transports are set up and how sessions are managed.
|
||||
- `instructions.md`
|
||||
- Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange.
|
||||
- `startup.md`
|
||||
- Describes the server startup sequence, environment checks, and initialization.
|
||||
- `structure.md`
|
||||
- This document.
|
||||
- `server-instructions.md`
|
||||
- Human‑readable instructions intended to be passed to the client/LLM as for guidance on server use. Loaded by the server at startup and returned in the "initialize" exchange.
|
||||
|
||||
### `prompts/`
|
||||
|
||||
|
|
@ -154,6 +165,8 @@ src/everything
|
|||
- Registers a `simulate-research-query` task-based tool that demonstrates the MCP Tasks feature (SEP-1686). Simulates a multi-stage research operation with progress updates. If the query is marked as ambiguous and the client supports elicitation, it pauses mid-execution to request clarification via `elicitation/create`. Uses `server.experimental.tasks.registerToolTask()` with `execution: { taskSupport: "required" }`.
|
||||
- `trigger-elicitation-request.ts`
|
||||
- Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result.
|
||||
- `trigger-url-elicitation.ts`
|
||||
- Registers a `trigger-url-elicitation` tool that either sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId` (request path) or throws `UrlElicitationRequiredError` (`-32042`) for client-handled URL elicitation (error path). On the error path the carried prerequisite elicitation points at a different URL than the failing one (`https://modelcontextprotocol.io`), and when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path — so the client does not loop on the same error.
|
||||
- `trigger-elicitation-request-async.ts`
|
||||
- Registers a `trigger-elicitation-request-async` tool that demonstrates bidirectional MCP tasks for elicitation. Sends an elicitation request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result.
|
||||
- `trigger-sampling-request.ts`
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"test": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.2.1",
|
||||
"jszip": "^3.10.1",
|
||||
|
|
@ -39,10 +39,10 @@
|
|||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"prettier": "^2.8.8",
|
||||
"shx": "^0.3.4",
|
||||
"shx": "^0.4.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^4.1.8"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,10 +99,9 @@ export const setSubscriptionHandlers = (server: McpServer) => {
|
|||
/**
|
||||
* Sends simulated resource update notifications to the subscribed client.
|
||||
*
|
||||
* This function iterates through all resource URIs stored in the subscriptions
|
||||
* and checks if the specified session ID is subscribed to them. If so, it sends
|
||||
* a notification through the provided server. If the session ID is no longer valid
|
||||
* (disconnected), it removes the session ID from the list of subscribers.
|
||||
* Iterates the URIs in `subscriptions` and emits a
|
||||
* `notifications/resources/updated` notification for each URI the given
|
||||
* sessionId is subscribed to.
|
||||
*
|
||||
* @param {McpServer} server - The server instance used to send notifications.
|
||||
* @param {string | undefined} sessionId - The session ID of the client to check for subscriptions.
|
||||
|
|
@ -122,8 +121,6 @@ const sendSimulatedResourceUpdates = async (
|
|||
method: "notifications/resources/updated",
|
||||
params: { uri },
|
||||
});
|
||||
} else {
|
||||
subscribers.delete(sessionId); // subscriber has disconnected
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,31 +127,26 @@ export const blobResourceUri = (resourceId: number) =>
|
|||
new URL(`${blobUriBase}/${resourceId}`);
|
||||
|
||||
/**
|
||||
* Parses the resource identifier from the provided URI and validates it
|
||||
* against the given variables. Throws an error if the URI corresponds
|
||||
* to an unknown resource or if the resource identifier is invalid.
|
||||
* Parses the resource identifier from the provided variables and validates
|
||||
* that it is a positive integer.
|
||||
*
|
||||
* @param {URL} uri - The URI of the resource to be parsed.
|
||||
* @param {Record<string, unknown>} variables - A record containing context-specific variables that include the resourceId.
|
||||
* @returns {number} The parsed and validated resource identifier as an integer.
|
||||
* @throws {Error} Throws an error if the URI matches unsupported base URIs or if the resourceId is invalid.
|
||||
* The SDK only routes URIs that match the registered template to the
|
||||
* resource handler, so by the time we get here the URI prefix is already
|
||||
* known to be one of `textUriBase` / `blobUriBase`. Only the resourceId
|
||||
* variable still needs validating.
|
||||
*
|
||||
* @param {URL} uri - The URI of the resource (used in the error message).
|
||||
* @param {Record<string, unknown>} variables - Context variables including resourceId.
|
||||
* @returns {number} The parsed and validated resource identifier as a positive integer.
|
||||
* @throws {Error} If the resourceId is not a finite positive integer.
|
||||
*/
|
||||
const parseResourceId = (uri: URL, variables: Record<string, unknown>) => {
|
||||
const uriError = `Unknown resource: ${uri.toString()}`;
|
||||
if (
|
||||
uri.toString().startsWith(textUriBase) &&
|
||||
uri.toString().startsWith(blobUriBase)
|
||||
) {
|
||||
throw new Error(uriError);
|
||||
} else {
|
||||
const idxStr = String((variables as any).resourceId ?? "");
|
||||
const idx = Number(idxStr);
|
||||
if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) {
|
||||
return idx;
|
||||
} else {
|
||||
throw new Error(uriError);
|
||||
}
|
||||
const idxStr = String((variables as any).resourceId ?? "");
|
||||
const idx = Number(idxStr);
|
||||
if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) {
|
||||
return idx;
|
||||
}
|
||||
throw new Error(`Unknown resource: ${uri.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.j
|
|||
import { registerTriggerSamplingRequestAsyncTool } from "./trigger-sampling-request-async.js";
|
||||
import { registerTriggerElicitationRequestAsyncTool } from "./trigger-elicitation-request-async.js";
|
||||
import { registerSimulateResearchQueryTool } from "./simulate-research-query.js";
|
||||
import { registerTriggerUrlElicitationTool } from "./trigger-url-elicitation.js";
|
||||
|
||||
/**
|
||||
* Register the tools with the MCP server.
|
||||
|
|
@ -44,6 +45,7 @@ export const registerTools = (server: McpServer) => {
|
|||
export const registerConditionalTools = (server: McpServer) => {
|
||||
registerGetRootsListTool(server);
|
||||
registerTriggerElicitationRequestTool(server);
|
||||
registerTriggerUrlElicitationTool(server);
|
||||
registerTriggerSamplingRequestTool(server);
|
||||
// Task-based research tool (uses experimental tasks API)
|
||||
registerSimulateResearchQueryTool(server);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue