feat!: add server discovery and negotiation (SEP-2575) (#973)
* feat!: add server discovery and negotiation * feat!: make protocol unions extensible * ci: add discovery conformance coverage
This commit is contained in:
parent
b93220d4a7
commit
3549e86d28
7 changed files with 1325 additions and 98 deletions
|
|
@ -562,6 +562,22 @@
|
|||
"CustomResult": {
|
||||
"description": "A catch-all response either side can use for custom requests."
|
||||
},
|
||||
"DiscoverRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "server/discover"
|
||||
},
|
||||
"DiscoverRequestParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"_meta"
|
||||
]
|
||||
},
|
||||
"ElicitResult": {
|
||||
"description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.",
|
||||
"type": "object",
|
||||
|
|
@ -1120,6 +1136,9 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam"
|
||||
},
|
||||
|
|
@ -1129,9 +1148,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam3"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request6"
|
||||
},
|
||||
|
|
@ -1141,21 +1157,24 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request8"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request9"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
"$ref": "#/definitions/RequestOptionalParam4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request10"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request11"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request12"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CustomRequest"
|
||||
}
|
||||
|
|
@ -1548,6 +1567,22 @@
|
|||
]
|
||||
},
|
||||
"Request10": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"Request11": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1563,7 +1598,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request11": {
|
||||
"Request12": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1580,6 +1615,22 @@
|
|||
]
|
||||
},
|
||||
"Request2": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/DiscoverRequestMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DiscoverRequestParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"Request3": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1595,7 +1646,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request3": {
|
||||
"Request4": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1611,7 +1662,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request4": {
|
||||
"Request5": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1627,7 +1678,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request5": {
|
||||
"Request6": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1643,7 +1694,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request6": {
|
||||
"Request7": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1659,7 +1710,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request7": {
|
||||
"Request8": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1675,7 +1726,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request8": {
|
||||
"Request9": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1691,22 +1742,6 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request9": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"RequestMetaObject": {
|
||||
"description": "Metadata reserved by MCP on requests. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -562,6 +562,22 @@
|
|||
"CustomResult": {
|
||||
"description": "A catch-all response either side can use for custom requests."
|
||||
},
|
||||
"DiscoverRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "server/discover"
|
||||
},
|
||||
"DiscoverRequestParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"_meta"
|
||||
]
|
||||
},
|
||||
"ElicitResult": {
|
||||
"description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.",
|
||||
"type": "object",
|
||||
|
|
@ -1120,6 +1136,9 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam"
|
||||
},
|
||||
|
|
@ -1129,9 +1148,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam3"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request6"
|
||||
},
|
||||
|
|
@ -1141,21 +1157,24 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request8"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request9"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
"$ref": "#/definitions/RequestOptionalParam4"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request10"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request11"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request12"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CustomRequest"
|
||||
}
|
||||
|
|
@ -1548,6 +1567,22 @@
|
|||
]
|
||||
},
|
||||
"Request10": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"Request11": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1563,7 +1598,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request11": {
|
||||
"Request12": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1580,6 +1615,22 @@
|
|||
]
|
||||
},
|
||||
"Request2": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/DiscoverRequestMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DiscoverRequestParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"Request3": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1595,7 +1646,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request3": {
|
||||
"Request4": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1611,7 +1662,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request4": {
|
||||
"Request5": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1627,7 +1678,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request5": {
|
||||
"Request6": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1643,7 +1694,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request6": {
|
||||
"Request7": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1659,7 +1710,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request7": {
|
||||
"Request8": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1675,7 +1726,7 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request8": {
|
||||
"Request9": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1691,22 +1742,6 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request9": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"RequestMetaObject": {
|
||||
"description": "Metadata reserved by MCP on requests. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -743,6 +743,83 @@
|
|||
"CustomResult": {
|
||||
"description": "A catch-all response either side can use for custom requests."
|
||||
},
|
||||
"DiscoverResult": {
|
||||
"description": "The server's response to a [`DiscoverRequest`].",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level response metadata.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cacheScope": {
|
||||
"description": "Whether the cached result may be shared across authorization contexts.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CacheScope"
|
||||
}
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"description": "Capabilities provided by this server.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServerCapabilities"
|
||||
}
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"description": "Optional guidance for using the server.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Identifies how the result should be parsed.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serverInfo": {
|
||||
"description": "Information about the server implementation.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Implementation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"supportedVersions": {
|
||||
"description": "Protocol versions implemented by this server.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ProtocolVersion"
|
||||
}
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "How long clients may consider this response fresh, in milliseconds.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resultType",
|
||||
"supportedVersions",
|
||||
"capabilities",
|
||||
"serverInfo",
|
||||
"ttlMs",
|
||||
"cacheScope"
|
||||
]
|
||||
},
|
||||
"ElicitRequestParams": {
|
||||
"description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```",
|
||||
"anyOf": [
|
||||
|
|
@ -3194,6 +3271,9 @@
|
|||
},
|
||||
"ServerResult": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DiscoverResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/InitializeResult"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -743,6 +743,83 @@
|
|||
"CustomResult": {
|
||||
"description": "A catch-all response either side can use for custom requests."
|
||||
},
|
||||
"DiscoverResult": {
|
||||
"description": "The server's response to a [`DiscoverRequest`].",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level response metadata.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cacheScope": {
|
||||
"description": "Whether the cached result may be shared across authorization contexts.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CacheScope"
|
||||
}
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"description": "Capabilities provided by this server.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServerCapabilities"
|
||||
}
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"description": "Optional guidance for using the server.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Identifies how the result should be parsed.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serverInfo": {
|
||||
"description": "Information about the server implementation.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Implementation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"supportedVersions": {
|
||||
"description": "Protocol versions implemented by this server.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ProtocolVersion"
|
||||
}
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "How long clients may consider this response fresh, in milliseconds.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resultType",
|
||||
"supportedVersions",
|
||||
"capabilities",
|
||||
"serverInfo",
|
||||
"ttlMs",
|
||||
"cacheScope"
|
||||
]
|
||||
},
|
||||
"ElicitRequestParams": {
|
||||
"description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```",
|
||||
"anyOf": [
|
||||
|
|
@ -3194,6 +3271,9 @@
|
|||
},
|
||||
"ServerResult": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DiscoverResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/InitializeResult"
|
||||
},
|
||||
|
|
|
|||
108
crates/rmcp/tests/test_server_discover.rs
Normal file
108
crates/rmcp/tests/test_server_discover.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use rmcp::model::{
|
||||
ClientCapabilities, ClientJsonRpcMessage, ClientRequest, DiscoverResult, ErrorCode, ErrorData,
|
||||
JsonRpcRequest, JsonRpcResponse, ProtocolVersion, ServerJsonRpcMessage, ServerResult,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn discover_request_deserializes_with_request_meta() {
|
||||
let message: ClientJsonRpcMessage = serde_json::from_value(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "server/discover",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientInfo": {
|
||||
"name": "test-client",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"io.modelcontextprotocol/clientCapabilities": {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("discover request should deserialize");
|
||||
|
||||
let ClientJsonRpcMessage::Request(JsonRpcRequest { request, .. }) = message else {
|
||||
panic!("expected request");
|
||||
};
|
||||
let ClientRequest::DiscoverRequest(request) = request else {
|
||||
panic!("expected discover request");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.extensions
|
||||
.get::<rmcp::model::RequestMetaObject>()
|
||||
.and_then(|meta| meta.protocol_version()),
|
||||
Some(ProtocolVersion::V_2026_07_28)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_result_deserializes_to_typed_variant() {
|
||||
let message: ServerJsonRpcMessage = serde_json::from_value(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resultType": "complete",
|
||||
"supportedVersions": ["2025-11-25", "2026-07-28"],
|
||||
"capabilities": { "tools": {} },
|
||||
"serverInfo": {
|
||||
"name": "test-server",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"ttlMs": 0,
|
||||
"cacheScope": "private"
|
||||
}
|
||||
}))
|
||||
.expect("discover result should deserialize");
|
||||
|
||||
let ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) = message else {
|
||||
panic!("expected response");
|
||||
};
|
||||
let ServerResult::DiscoverResult(DiscoverResult {
|
||||
supported_versions, ..
|
||||
}) = result
|
||||
else {
|
||||
panic!("expected discover result");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
supported_versions,
|
||||
vec![ProtocolVersion::V_2025_11_25, ProtocolVersion::V_2026_07_28]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_protocol_version_error_matches_draft_schema() {
|
||||
let error = ErrorData::unsupported_protocol_version(
|
||||
ProtocolVersion::V_2026_07_28,
|
||||
&[ProtocolVersion::V_2025_11_25],
|
||||
);
|
||||
|
||||
assert_eq!(error.code, ErrorCode::UNSUPPORTED_PROTOCOL_VERSION);
|
||||
assert_eq!(
|
||||
error.data,
|
||||
Some(json!({
|
||||
"requested": "2026-07-28",
|
||||
"supported": ["2025-11-25"]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_required_capability_error_matches_draft_schema() {
|
||||
let required = ClientCapabilities::builder().enable_elicitation().build();
|
||||
let error = ErrorData::missing_required_client_capability(required);
|
||||
|
||||
assert_eq!(error.code, ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY);
|
||||
assert_eq!(
|
||||
error.data,
|
||||
Some(json!({
|
||||
"requiredCapabilities": {
|
||||
"elicitation": {}
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
77
crates/rmcp/tests/test_server_discover_client.rs
Normal file
77
crates/rmcp/tests/test_server_discover_client.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#![cfg(all(feature = "client", not(feature = "local")))]
|
||||
|
||||
use rmcp::{
|
||||
ClientHandler, ServerHandler, ServiceExt,
|
||||
model::{
|
||||
ClientCapabilities, Implementation, ProtocolVersion, RequestMetaObject, ServerCapabilities,
|
||||
ServerInfo,
|
||||
},
|
||||
select_protocol_version,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DiscoveryServer;
|
||||
|
||||
impl ServerHandler for DiscoveryServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_server_info(Implementation::new("discovery-server", "1.0.0"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DiscoveryClient;
|
||||
|
||||
impl ClientHandler for DiscoveryClient {}
|
||||
|
||||
#[test]
|
||||
fn select_protocol_version_uses_client_preference_order() {
|
||||
let selected = select_protocol_version(
|
||||
&[ProtocolVersion::V_2026_07_28, ProtocolVersion::V_2025_11_25],
|
||||
&[ProtocolVersion::V_2025_11_25, ProtocolVersion::V_2026_07_28],
|
||||
);
|
||||
|
||||
assert_eq!(selected, Some(ProtocolVersion::V_2026_07_28));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_protocol_version_returns_none_without_overlap() {
|
||||
let selected = select_protocol_version(
|
||||
&[ProtocolVersion::V_2026_07_28],
|
||||
&[ProtocolVersion::V_2025_11_25],
|
||||
);
|
||||
|
||||
assert_eq!(selected, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_discover_helper_returns_typed_result() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
tokio::spawn(async move {
|
||||
let _ = DiscoveryServer
|
||||
.serve(server_transport)
|
||||
.await
|
||||
.expect("server should start")
|
||||
.waiting()
|
||||
.await;
|
||||
});
|
||||
let client = DiscoveryClient
|
||||
.serve(client_transport)
|
||||
.await
|
||||
.expect("client should connect");
|
||||
let mut meta = RequestMetaObject::new();
|
||||
meta.set_protocol_version(ProtocolVersion::V_2026_07_28);
|
||||
meta.set_client_info(Implementation::new("discovery-client", "1.0.0"));
|
||||
meta.set_client_capabilities(ClientCapabilities::default());
|
||||
|
||||
let result = client
|
||||
.discover(meta)
|
||||
.await
|
||||
.expect("discover should succeed");
|
||||
|
||||
assert_eq!(
|
||||
result.server_info,
|
||||
Implementation::new("discovery-server", "1.0.0")
|
||||
);
|
||||
client.cancel().await.expect("client should cancel");
|
||||
}
|
||||
346
crates/rmcp/tests/test_server_discover_http.rs
Normal file
346
crates/rmcp/tests/test_server_discover_http.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
#![cfg(all(
|
||||
not(feature = "local"),
|
||||
feature = "reqwest",
|
||||
feature = "transport-streamable-http-server"
|
||||
))]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo},
|
||||
transport::streamable_http_server::{
|
||||
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
|
||||
},
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DiscoveryServer;
|
||||
|
||||
impl ServerHandler for DiscoveryServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_server_info(Implementation::new("discovery-server", "1.0.0"))
|
||||
.with_instructions("Use the tools carefully")
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(&[ProtocolVersion::V_2025_11_25])
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_server(json_response: bool) -> (reqwest::Client, String, CancellationToken) {
|
||||
spawn_server_with_stateful_mode(json_response, false).await
|
||||
}
|
||||
|
||||
async fn spawn_server_with_stateful_mode(
|
||||
json_response: bool,
|
||||
stateful_mode: bool,
|
||||
) -> (reqwest::Client, String, CancellationToken) {
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let config = StreamableHttpServerConfig::default()
|
||||
.with_stateful_mode(stateful_mode)
|
||||
.with_json_response(json_response)
|
||||
.with_sse_keep_alive(None)
|
||||
.with_cancellation_token(cancellation_token.clone());
|
||||
let service: StreamableHttpService<DiscoveryServer, LocalSessionManager> =
|
||||
StreamableHttpService::new(|| Ok(DiscoveryServer), Default::default(), config);
|
||||
let router = axum::Router::new().nest_service("/mcp", service);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let address = listener.local_addr().expect("listener should have address");
|
||||
|
||||
tokio::spawn({
|
||||
let cancellation_token = cancellation_token.clone();
|
||||
async move {
|
||||
let _ = axum::serve(listener, router)
|
||||
.with_graceful_shutdown(async move {
|
||||
cancellation_token.cancelled_owned().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
reqwest::Client::new(),
|
||||
format!("http://{address}/mcp"),
|
||||
cancellation_token,
|
||||
)
|
||||
}
|
||||
|
||||
fn discover_body(version: Option<&str>) -> serde_json::Value {
|
||||
let meta = version.map(|version| {
|
||||
json!({
|
||||
"io.modelcontextprotocol/protocolVersion": version,
|
||||
"io.modelcontextprotocol/clientInfo": {
|
||||
"name": "test-client",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"io.modelcontextprotocol/clientCapabilities": {}
|
||||
})
|
||||
});
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "server/discover",
|
||||
"params": meta.map(|meta| json!({ "_meta": meta })).unwrap_or_else(|| json!({}))
|
||||
})
|
||||
}
|
||||
|
||||
async fn post_discover(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
header_version: &str,
|
||||
body_version: Option<&str>,
|
||||
) -> reqwest::Response {
|
||||
client
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", header_version)
|
||||
.header("Mcp-Method", "server/discover")
|
||||
.json(&discover_body(body_version))
|
||||
.send()
|
||||
.await
|
||||
.expect("discover request should send")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_returns_server_metadata_without_session() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2025-11-25", Some("2025-11-25")).await;
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(
|
||||
body["result"],
|
||||
json!({
|
||||
"resultType": "complete",
|
||||
"supportedVersions": ["2025-11-25"],
|
||||
"capabilities": { "tools": {} },
|
||||
"serverInfo": {
|
||||
"name": "discovery-server",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"instructions": "Use the tools carefully",
|
||||
"ttlMs": 0,
|
||||
"cacheScope": "private"
|
||||
})
|
||||
);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_does_not_require_initialization_in_stateful_mode() {
|
||||
let (client, url, cancellation_token) = spawn_server_with_stateful_mode(true, true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2025-11-25", Some("2025-11-25")).await;
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert!(response.headers().get("Mcp-Session-Id").is_none());
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_rejects_unsupported_version_with_http_400() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32022);
|
||||
assert_eq!(
|
||||
body["error"]["data"],
|
||||
json!({
|
||||
"requested": "2026-07-28",
|
||||
"supported": ["2025-11-25"]
|
||||
})
|
||||
);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_rejects_unknown_version_with_typed_error() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2099-01-01", Some("2099-01-01")).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32022);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_request_rejects_server_unsupported_meta_version() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2026-07-28")
|
||||
.header("Mcp-Method", "tools/list")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should send");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32022);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_rpc_uses_http_404_for_per_request_protocol() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "unknown/method",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2025-11-25"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2025-11-25")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should send");
|
||||
|
||||
assert_eq!(response.status(), 404);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_unknown_rpc_preserves_http_200_jsonrpc_error() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "unknown/method",
|
||||
"params": {}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2025-11-25")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should send");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32601);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_rejects_header_meta_version_mismatch() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2026-07-28", Some("2025-11-25")).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_rejects_missing_request_meta() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2025-11-25", None).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_rejects_missing_client_capabilities() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "server/discover",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2025-11-25",
|
||||
"io.modelcontextprotocol/clientInfo": {
|
||||
"name": "test-client",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2025-11-25")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should send");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_error_uses_http_400_when_sse_is_configured() {
|
||||
let (client, url, cancellation_token) = spawn_server(false).await;
|
||||
|
||||
let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("application/json")
|
||||
);
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
Loading…
Reference in a new issue