Compare commits

..
Author SHA1 Message Date
GitHub Actions
0e5f1582b8 Automatic update of packages 2026-06-04 01:19:41 +00:00
62 changed files with 3286 additions and 5367 deletions

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'

View file

@ -2,13 +2,11 @@ name: Python
on:
push:
branches:
- main
pull_request:
# 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' }}
release:
types: [published]
jobs:
detect-packages:
@ -36,10 +34,10 @@ jobs:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v3
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
@ -73,10 +71,10 @@ jobs:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v3
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
@ -93,7 +91,31 @@ jobs:
run: uv build
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
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

View file

@ -1,10 +1,7 @@
name: README PR Check
on:
# 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:
pull_request:
types: [opened]
paths:
- 'README.md'
@ -13,7 +10,7 @@ on:
jobs:
check-readme-only:
if: github.event_name == 'pull_request_target'
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read

View file

@ -1,7 +1,9 @@
name: Release
name: Automatic Release Creation
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * *'
jobs:
create-metadata:
@ -25,7 +27,7 @@ jobs:
echo "Using last release hash: ${HASH}"
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v5
- name: Create version name
id: create-version
@ -41,7 +43,7 @@ jobs:
cat RELEASE_NOTES.md
- name: Release notes
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: release-notes
path: RELEASE_NOTES.md
@ -77,7 +79,7 @@ jobs:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v5
- name: Update packages
run: |
@ -121,10 +123,10 @@ jobs:
ref: ${{ needs.create-metadata.outputs.version }}
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v5
- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version-file: "src/${{ matrix.package }}/.python-version"
@ -136,15 +138,6 @@ 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
@ -153,7 +146,6 @@ 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]
@ -164,25 +156,18 @@ 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@v7
- uses: actions/setup-node@v6
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
@ -197,22 +182,16 @@ 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:
NPM_CONFIG_PROVENANCE: "true"
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [update-packages, create-metadata, publish-pypi, publish-npm]
@ -228,7 +207,7 @@ jobs:
- uses: actions/checkout@v6
- name: Download release notes
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: release-notes

View file

@ -2,13 +2,11 @@ name: TypeScript
on:
push:
branches:
- main
pull_request:
# 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' }}
release:
types: [published]
jobs:
detect-packages:
@ -34,7 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
@ -57,7 +55,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
@ -70,3 +68,35 @@ 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 }}

View file

@ -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/brave-search-mcp-server`](https://www.npmjs.com/package/@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).
- **[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,10 +145,6 @@ 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.

View file

@ -1,49 +0,0 @@
# 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.

3772
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,9 +23,5 @@
"@modelcontextprotocol/server-memory": "*",
"@modelcontextprotocol/server-filesystem": "*",
"@modelcontextprotocol/server-sequential-thinking": "*"
},
"overrides": {
"qs": ">=6.15.2",
"hono": ">=4.12.21"
}
}

View file

@ -113,9 +113,7 @@ def has_changes(path: Path, git_hash: GitHash) -> bool:
)
changed_files = [Path(f) for f in output.stdout.splitlines()]
# .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"]]
relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts"]]
return len(relevant_files) >= 1
except subprocess.CalledProcessError:
return False

View file

@ -113,7 +113,7 @@ npm run start:streamableHttp
### Install
```shell
npm install -g @modelcontextprotocol/server-everything@latest
```
````
### Run the default (stdio) server
```shell
@ -134,7 +134,3 @@ 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.

View file

@ -54,7 +54,7 @@ describe('Registration Index Files', () => {
server: {
getClientCapabilities: vi.fn(() => ({
roots: {},
elicitation: { url: {} },
elicitation: {},
sampling: {},
})),
},
@ -67,17 +67,14 @@ describe('Registration Index Files', () => {
registerConditionalTools(mockServerWithCapabilities);
// 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);
// Should register 3 conditional tools + 3 task-based tools when all capabilities present
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(3);
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

View file

@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi } 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,13 +13,8 @@ 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() {
@ -711,318 +706,6 @@ 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();
@ -1055,90 +738,6 @@ 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[] = [];

View file

@ -19,11 +19,9 @@
- `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-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-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`.

View file

@ -54,7 +54,7 @@
- `prompts: {}`
- `resources: { subscribe: true }`
- **Server Instructions**
- Loaded from the docs folder (`instructions.md`).
- Loaded from the docs folder (`server-instructions.md`).
- **Registrations**
- Registers **tools** via `registerTools(server)`.
- Registers **resources** via `registerResources(server)`.

View file

@ -55,8 +55,7 @@ src/everything
│ ├── trigger-elicitation-request-async.ts
│ ├── trigger-long-running-operation.ts
│ ├── trigger-sampling-request.ts
│ ├── trigger-sampling-request-async.ts
│ └── trigger-url-elicitation.ts
│ └── trigger-sampling-request-async.ts
└── transports
├── sse.ts
├── stdio.ts
@ -85,19 +84,9 @@ 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/`
@ -165,8 +154,6 @@ 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`

View file

@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-everything",
"version": "2.0.0",
"version": "2026.6.4",
"description": "MCP server that exercises all the features of the MCP protocol",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-everything",
@ -30,7 +30,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@modelcontextprotocol/sdk": "^1.29.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": "^4.1.8",
"@vitest/coverage-v8": "^2.1.8",
"prettier": "^2.8.8",
"shx": "^0.4.0",
"shx": "^0.3.4",
"typescript": "^5.6.2",
"vitest": "^4.1.8"
"vitest": "^2.1.8"
}
}
}

View file

@ -99,9 +99,10 @@ export const setSubscriptionHandlers = (server: McpServer) => {
/**
* Sends simulated resource update notifications to the subscribed client.
*
* Iterates the URIs in `subscriptions` and emits a
* `notifications/resources/updated` notification for each URI the given
* sessionId is subscribed to.
* 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.
*
* @param {McpServer} server - The server instance used to send notifications.
* @param {string | undefined} sessionId - The session ID of the client to check for subscriptions.
@ -121,6 +122,8 @@ const sendSimulatedResourceUpdates = async (
method: "notifications/resources/updated",
params: { uri },
});
} else {
subscribers.delete(sessionId); // subscriber has disconnected
}
}
};

View file

@ -127,26 +127,31 @@ export const blobResourceUri = (resourceId: number) =>
new URL(`${blobUriBase}/${resourceId}`);
/**
* Parses the resource identifier from the provided variables and validates
* that it is a positive integer.
* 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.
*
* 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.
* @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.
*/
const parseResourceId = (uri: URL, variables: Record<string, unknown>) => {
const idxStr = String((variables as any).resourceId ?? "");
const idx = Number(idxStr);
if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) {
return idx;
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);
}
}
throw new Error(`Unknown resource: ${uri.toString()}`);
};
/**

View file

@ -17,7 +17,6 @@ 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.
@ -45,7 +44,6 @@ 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