Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions nitrostack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@
from nitrostack.tasks import InMemoryTaskStore, TaskAccessContext, TaskStore
from nitrostack.tasks.authorization import check_task_access, extract_task_access_context
from nitrostack.runtime import StatelessInvariants, assert_stateless_headers
from nitrostack.transports import wrap_stateless_transport, StatelessIngressPipeline


__all__ = [
Expand Down Expand Up @@ -242,6 +241,4 @@
"extract_task_access_context",
"StatelessInvariants",
"assert_stateless_headers",
"wrap_stateless_transport",
"StatelessIngressPipeline",
]
60 changes: 19 additions & 41 deletions nitrostack/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
strip_tool_arguments,
)
from nitrostack.protocol.observability import TraceContext, extract_trace_context
from nitrostack.runtime.correlation import InFlightRegistry, new_correlation_id
from nitrostack.runtime.correlation import new_correlation_id
from nitrostack.transports.headers import (
extract_mcp_param_headers,
extract_mcp_scope_headers,
Expand Down Expand Up @@ -541,7 +541,6 @@ def __init__(self, app_class: Type):
self._prompts: Dict[str, _PromptEntry] = {}
self._initial_tools: List[Tuple[Any, Callable, ToolConfig]] = []
self.task_manager = TaskManager()
self._in_flight = InFlightRegistry()

self._bootstrap()

Expand Down Expand Up @@ -766,6 +765,11 @@ async def health_status_resource(context: ExecutionContext) -> str:
# ------------------------------------------------------------------

def _advertise_tasks_extension(self) -> bool:
# Tasks remain available to in-process callers and the legacy wire only.
# MCP 2026 has no complete tasks surface (notably tasks/update), so
# advertising this partial implementation would make discovery lie.
if self.protocol_era in ("auto", "modern"):
return False
return any(
entry.config.task_support in ("optional", "required")
for entry in self._tools.values()
Expand Down Expand Up @@ -1275,6 +1279,11 @@ async def _call_tool(self, name: str, arguments: Dict[str, Any], task: Any = Non
trace = _trace_context_from_request_ctx(rc)

is_task = task_metadata is not None
if self.protocol_era in ("auto", "modern") and is_task:
raise MCPError(
types.INVALID_PARAMS,
"Server does not support task augmentation on the MCP 2026 wire",
)
if cfg.task_support == "forbidden" and task_metadata is not None:
raise MCPError(
types.METHOD_NOT_FOUND,
Expand Down Expand Up @@ -1369,7 +1378,6 @@ async def background_execution():
trace=trace,
)
_apply_request_envelope(ctx, rc)
ticket = self._in_flight.register(correlation_id, jsonrpc_id=jsonrpc_id)
try:
result = await run_pipeline(
handler=entry.method,
Expand All @@ -1391,13 +1399,6 @@ async def background_execution():
content=[types.TextContent(type="text", text=str(exc))],
isError=True,
)
finally:
self._in_flight.discard(correlation_id)
if ticket.cancel_requested.is_set():
return types.CallToolResult(
content=[types.TextContent(type="text", text="Request was cancelled.")],
isError=True,
)
return self._to_call_tool_result(result, entry.component, ctx)

async def _read_resource(self, uri: str) -> List[ReadResourceContents]:
Expand Down Expand Up @@ -1546,6 +1547,10 @@ def _build_get_task_result(self, task) -> types.GetTaskResult:
return GetTaskResult(**payload)

def _register_task_handlers(self, server: NitroStackMcpServer) -> None:
if self.protocol_era in ("auto", "modern"):
server.has_task_support = False
return

async def handle_list_tasks(req):
if rejects_deprecated_method("tasks/list", self.protocol_era):
message = deprecated_method_message("tasks/list")
Expand Down Expand Up @@ -1769,37 +1774,10 @@ def get_combined_app(
http_engine=http_engine,
)

if http_engine == "sessionless":
from nitrostack.transports.middleware import wrap_stateless_transport

def _discover_handler(_request):
return self.handle_server_discover()

def _initialize_handler(request):
params = getattr(request, "params", None) or {}
requested = params.get("protocolVersion") if isinstance(params, dict) else None
return self.handle_sessionless_initialize(
requested if isinstance(requested, str) else None
)

http_app = wrap_stateless_transport(
http_app,
server_name=self.server_config.name,
server_version=self.server_config.version,
protocol_version=protocol_version_for_era(era, self.server_config.protocol_version),
advertise_tasks=self._advertise_tasks_extension(),
advertise_app=any(
getattr(entry, "component", None) is not None
for entry in getattr(self, "_tools", {}).values()
),
custom_extensions=self._custom_extensions(),
wire_mode=wire_mode,
protocol_era=era,
enable_cors=enable_cors,
discover_handler=_discover_handler,
initialize_handler=_initialize_handler,
)

# The official MCP SDK owns the complete Streamable HTTP request lifecycle
# mounted by ``build_http_app``. Do not wrap it in a second JSON-RPC
# dispatcher: a sidecar would answer ping/initialize/tools/call itself and
# could diverge from the protocol and session behaviour of the SDK.
return http_app

async def _run_stdio(self) -> None:
Expand Down
5 changes: 3 additions & 2 deletions nitrostack/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ class TaskContext:
whichever transport (STDIO or Streamable HTTP) initiated the task —
transport-agnostic since both use the same ``mcp.server.session.ServerSession``.
The client only receives these if it supplied a ``progressToken`` in the
original ``tools/call`` request's ``_meta``; ``TaskManager``-backed polling via
``tasks/get`` always works regardless, so this is additive, not required.
original ``tools/call`` request's ``_meta``. TaskManager-backed polling is
retained for in-process callers and the legacy wire; the incomplete Tasks
surface is deliberately not advertised on MCP 2026.
"""

def __init__(
Expand Down
2 changes: 0 additions & 2 deletions nitrostack/protocol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from nitrostack.protocol.extensions import MCPExtensionId
from nitrostack.protocol.jsonrpc import (
JsonRpcWireError,
build_ping_response,
build_tool_error_result,
jsonrpc_error,
jsonrpc_success,
Expand Down Expand Up @@ -166,7 +165,6 @@
"build_sessionless_initialize_result",
"build_discover_result",
"parse_jsonrpc_request",
"build_ping_response",
"jsonrpc_success",
"jsonrpc_error",
"build_tool_error_result",
Expand Down
5 changes: 0 additions & 5 deletions nitrostack/protocol/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,6 @@ def jsonrpc_error(request_id: Any, code: int, message: str, data: Any = None) ->
return {"jsonrpc": JSONRPC_VERSION, "id": request_id, "error": error}


def build_ping_response(request_id: Any) -> dict[str, Any]:
"""Ping fast path."""
return jsonrpc_success(request_id, {})


def build_tool_error_result(message: str, *, text_type: str = "text") -> dict[str, Any]:
"""
Tool business failure — JSON-RPC success with isError: true.
Expand Down
12 changes: 3 additions & 9 deletions nitrostack/protocol/method_contract.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""SEP-2243 method contracts for the 2026 method surface.

Official v2 owns this table once mounted. Until then the sidecar validator
reads one row per method: required ``Mcp-Method``, optional ``Mcp-Name`` field,
and whether ``auto`` requires the method header.
The official MCP SDK owns request dispatch. This table records the method/header
contract used by the 2026 protocol checks: required ``Mcp-Method``, optional
``Mcp-Name`` field, and whether ``auto`` requires the method header.
"""

from __future__ import annotations
Expand Down Expand Up @@ -63,10 +63,6 @@ def _contract(
_contract("prompts/get", name_field=NAME_FIELD_NAME),
_contract("notifications/prompts/list_changed"),
_contract("completion/complete"),
_contract("tasks/get"),
_contract("tasks/cancel"),
_contract("tasks/result"),
_contract("tasks/list"),
_contract("logging/setLevel"),
)

Expand All @@ -82,8 +78,6 @@ def _contract(
# (``initialize``, ``notifications/initialized``) are not in this table:
# ``modern`` rejects them as method-not-found; ``auto`` still answers them.
DEPRECATED_MODERN_METHODS: dict[str, str] = {
"tasks/result": "Method 'tasks/result' is not supported in MCP 2026-07-28; use 'tasks/get'.",
"tasks/list": "Method 'tasks/list' is not supported in modern stateless MCP 2026-07-28.",
"resources/subscribe": (
"Method 'resources/subscribe' is not supported in stateless MCP 2026-07-28; "
"use SSE subscriptions/listen."
Expand Down
4 changes: 2 additions & 2 deletions nitrostack/runtime/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class AcceptanceCriterion:
),
ProtocolDeliverable(
ProtocolArea.ASYNC_TASKS,
"Task state machine and tasks/get embedded result",
"Legacy/in-process Task state machine and embedded result",
"tests/test_mcp20_tasks.py",
),
ProtocolDeliverable(
Expand Down Expand Up @@ -146,7 +146,7 @@ class AcceptanceCriterion:
),
AcceptanceCriterion(
"task_cancellation",
"tasks/cancel marks cancelled; terminal cancel returns -32602",
"Legacy tasks/cancel marks cancelled; terminal cancel returns -32602",
"tests/test_mcp20_tasks.py",
),
AcceptanceCriterion(
Expand Down
2 changes: 1 addition & 1 deletion nitrostack/runtime/conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ConformanceArea(str, Enum):
"Standard JSON-RPC error codes; deprecated methods rejected on modern wire"
),
ConformanceArea.TASKS: (
"Task-augmented tools/call, tasks/get lifecycle, cancel, terminal-only TTL eviction"
"MCP 2026 hides the incomplete Tasks surface; legacy/in-process task lifecycle remains"
),
ConformanceArea.MULTI_TENANT: (
"Cross-tenant task access raises TaskNotFoundError without enumeration"
Expand Down
3 changes: 2 additions & 1 deletion nitrostack/runtime/stateless.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ def request_protocol_version(

The header wins when present. Otherwise ``_meta.mcp.protocolVersion`` is
used. When neither is present the request proceeds (legacy default). The
header is not required on ``modern`` in this sidecar.
header is not required on ``modern`` because the official SDK owns the
HTTP request lifecycle.
"""
for raw in (header_version, envelope_version):
if isinstance(raw, str) and raw.strip():
Expand Down
Loading