MCP extension test packs
The MCP 2026-07-28 spec promotes Extensions from a side door to a first-class feature (SEP-2133). Every capability beyond the core set is named by a reverse-DNS id, negotiated through the extensions capability map, and versioned independently of the spec.
mcptest treats extensions as pluggable test packs: each pack handles one extension id and runs after the server advertises it. The OSS engine detects advertised extensions and reports them in mcptest capabilities and mcptest inspect. The Tasks pack ships once the lifecycle checks land; the MCP Apps validation pack is not bundled here.
This page describes the framework: how detection works, how to write a new pack, and the two extensions the 2026-07-28 spec ships.
Detection (advertised extensions)
The framework looks for an extensions object inside the server's capability block (handed back from initialize or server/discover):
{
"capabilities": {
"tools": {},
"extensions": {
"io.modelcontextprotocol.tasks": { "version": "1.0" },
"io.modelcontextprotocol.apps": {},
"com.example.widgets": { "version": "2.3" }
}
}
}
Every reverse-DNS key (any key containing a .) is treated as an extension id. The version string under each id is captured when present. Non-reverse-DNS keys are skipped so a server that mistakenly emits a flat tasks key under extensions does not produce a noisy report row.
mcptest capabilities and mcptest inspect print a labelled Extensions: section after the main capability list:
Capabilities:
tools (listChanged: true)
resources
Extensions:
Tasks (v1.0): io.modelcontextprotocol.tasks
MCP Apps: io.modelcontextprotocol.apps
extension (v2.3): com.example.widgets
Known extensions
The 2026-07-28 spec ships two official extensions:
| Extension | Wire id (canonical) | mcptest pack |
|---|---|---|
| Tasks (SEP-2133) | io.modelcontextprotocol.tasks | OSS |
| MCP Apps (SEP-1865) | io.modelcontextprotocol.apps | Not bundled |
mcptest classifies an id by its suffix so the framework still labels an extension correctly even if a server adopts a slightly different reverse-DNS form. Unknown ids surface as extension: <id> so the operator always sees the raw id.
Writing a pack
A pack implements the ExtensionPack trait in mcptest_core::extensions:
use mcptest_core::extensions::{ExtensionContext, ExtensionId, ExtensionPack};
pub struct MyPack {
id: ExtensionId,
}
impl ExtensionPack for MyPack {
fn id(&self) -> &ExtensionId {
&self.id
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn run(&self, ctx: &ExtensionContext) -> Result<(), String> {
// Inspect ctx.server_advertisement, run the pack's checks,
// return Ok(()) on success or Err(message) on failure.
Ok(())
}
}
The runner negotiates packs as follows:
- Call
initialize(orserver/discoveron 2026-07-28). - Walk advertised extensions via
mcptest_core::extensions::advertised_extensions. - For each pack whose
id()matches an advertised extension, invokerun. Every pack whose id is not advertised is skipped cleanly so running a suite against a smaller server does not flood the report withnot advertisedrows.
Packs are versioned independently of the spec: a single mcptest release can ship multiple versions of the same pack, and an operator can pin a suite to a specific pack version.
Status
- OSS detection of advertised extensions: shipped.
- Tasks pack: v1 shipped, negotiates the extension and verifies advertisement plus version. Full lifecycle checks (
tools/callreturning a task handle,tasks/get/tasks/update/tasks/cancelpolling) land in v2 once a Tasks-aware target server is reachable. - MCP Apps validation pack: not bundled.