mcptest docs GitHub

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:

ExtensionWire id (canonical)mcptest pack
Tasks (SEP-2133)io.modelcontextprotocol.tasksOSS
MCP Apps (SEP-1865)io.modelcontextprotocol.appsNot 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:

  1. Call initialize (or server/discover on 2026-07-28).
  2. Walk advertised extensions via mcptest_core::extensions::advertised_extensions.
  3. For each pack whose id() matches an advertised extension, invoke run. Every pack whose id is not advertised is skipped cleanly so running a suite against a smaller server does not flood the report with not advertised rows.

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