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",
]
81 changes: 42 additions & 39 deletions nitrostack/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
from pathlib import Path
from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Pattern, Set, Tuple, Type

from nitrostack.protocol.errors import JsonRpcErrorCode

import mcp.types as types
from mcp import MCPError
from mcp.server.context import ServerRequestContext

from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.types import (
CallToolRequestParams,
Expand All @@ -25,7 +28,6 @@
UnsubscribeRequestParams,
)
from pydantic import BaseModel, create_model

from nitrostack.core.context import ExecutionContext, TaskContext
from nitrostack.core.decorators import ToolConfig, ResourceConfig, PromptConfig, widget_resource_uri
from nitrostack.core.app_mode import get_app_mode, get_widget_mime_type, is_mcp_app_mode, is_openai_mode
Expand Down Expand Up @@ -845,15 +847,29 @@ def handle_server_discover(self, protocol_version: Optional[str] = None) -> Dict
getattr(self, "protocol_era", None),
self.server_config.protocol_version,
)
return build_discover_result(
server_name=self.server_config.name,
server_version=self.server_config.version,
protocol_version=version,
advertise_tasks=self._advertise_tasks_extension(),
advertise_app=has_widgets,
custom_extensions=self._custom_extensions(),
return self._stamp_discover_meta(
build_discover_result(
server_name=self.server_config.name,
server_version=self.server_config.version,
protocol_version=version,
advertise_tasks=self._advertise_tasks_extension(),
advertise_app=has_widgets,
custom_extensions=self._custom_extensions(),
)
)

def _stamp_discover_meta(self, result: Dict[str, Any]) -> Dict[str, Any]:
"""2026 puts serverInfo and resultType on the result, keeping any cache hint in _meta."""
out = dict(result)
meta = dict(out.get("_meta") or {})
meta["io.modelcontextprotocol/serverInfo"] = {
"name": self.server_config.name,
"version": self.server_config.version,
}
out["_meta"] = meta
out.setdefault("resultType", "complete")
return out

def handle_sessionless_initialize(self, requested_version: Optional[str] = None) -> Dict[str, Any]:
"""Answer 2025 ``initialize`` on the sessionless ``auto`` path. No session."""
from nitrostack.protocol.discovery import build_sessionless_initialize_result
Expand Down Expand Up @@ -885,6 +901,10 @@ async def _list_tools(_ctx: ServerRequestContext, _params: Optional[PaginatedReq
tools=[self._build_tool_definition(entry) for entry in self._tools.values()],
_meta=self._list_endpoint_cache_meta(),
)
async def _ping(_ctx: ServerRequestContext, _params: Optional[RequestParams]):
if self.protocol_era == "legacy":
return types.EmptyResult()
raise MCPError(types.METHOD_NOT_FOUND, "ping was removed in MCP 2026-07-28")

async def _call_tool(ctx: ServerRequestContext, params: CallToolRequestParams):
token = request_ctx.set(ctx)
Expand Down Expand Up @@ -962,6 +982,7 @@ async def _discover(ctx: ServerRequestContext, _params: Optional[RequestParams])
server.add_request_handler("tools/list", PaginatedRequestParams, _list_tools)
server.add_request_handler("tools/call", CallToolRequestParams, _call_tool)
server.add_request_handler("resources/list", PaginatedRequestParams, _list_resources)
server.add_request_handler("ping", RequestParams, _ping)
server.add_request_handler(
"resources/templates/list", PaginatedRequestParams, _list_resource_templates
)
Expand Down Expand Up @@ -1327,6 +1348,15 @@ async def _call_tool(self, name: str, arguments: Dict[str, Any], task: Any = Non
meta, "progressToken", None
)
session = getattr(rc, "session", None)


if task_metadata is not None and not getattr(self.mcp_server, "has_task_support", True):
raise MCPError(
JsonRpcErrorCode.MISSING_REQUIRED_CLIENT_CAPABILITY,
f"Tasks are not available on this protocol version; tool '{cfg.name}' "
"cannot be run as a task.",
)

auth_meta = _auth_metadata_from_request_ctx(rc)
trace = _trace_context_from_request_ctx(rc)

Expand Down Expand Up @@ -1825,37 +1855,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
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
2 changes: 2 additions & 0 deletions nitrostack/protocol/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class JsonRpcErrorCode(IntEnum):
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
HEADER_BODY_MISMATCH = -32020
MISSING_REQUIRED_CLIENT_CAPABILITY = -32021
UNSUPPORTED_PROTOCOL_VERSION = -32022


Expand All @@ -22,5 +23,6 @@ class JsonRpcErrorCode(IntEnum):
JsonRpcErrorCode.INVALID_PARAMS: "Invalid params",
JsonRpcErrorCode.INTERNAL_ERROR: "Internal error",
JsonRpcErrorCode.HEADER_BODY_MISMATCH: "Header/body mismatch",
JsonRpcErrorCode.MISSING_REQUIRED_CLIENT_CAPABILITY: "Missing required client capability",
JsonRpcErrorCode.UNSUPPORTED_PROTOCOL_VERSION: "Unsupported protocol version",
}
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
16 changes: 1 addition & 15 deletions nitrostack/transports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
"""MCP transport adapters (stdio, stateless HTTP)."""

from nitrostack.transports.dispatch import (
DispatchStage,
IngressContext,
StatelessIngressPipeline,
is_task_wire_interception,
)
from nitrostack.transports.middleware import StatelessTransportMiddleware, wrap_stateless_transport
"""MCP transport adapters (stdio and the official Streamable HTTP engine)."""
from nitrostack.transports.sse import format_sse_message, sse_connect_headers, sse_notification
from nitrostack.transports.subscriptions import (
http_listen_requires_auth,
Expand All @@ -15,12 +7,6 @@
)

__all__ = [
"DispatchStage",
"IngressContext",
"StatelessIngressPipeline",
"StatelessTransportMiddleware",
"wrap_stateless_transport",
"is_task_wire_interception",
"format_sse_message",
"sse_connect_headers",
"sse_notification",
Expand Down
Loading
Loading