use super::auth::{attach_legacy_deprecation_headers, should_stream_post_response};
use super::*;
use crate::{ApiKeyAuthConfig, AuthMethodConfig, DispatchCoreConfig};
use std::collections::BTreeMap;
#[tokio::test]
async fn tools_list_exposes_public_functions() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(McpServerConfig::new(core));
let tools = server.tools_list_result(&json!({}));
assert_eq!(tools["tools"][0]["name"], "greet");
assert_eq!(tools["tools"][0]["annotations"]["readOnlyHint"], false);
assert_eq!(tools["tools"][0]["annotations"]["destructiveHint"], true);
assert_eq!(tools["tools"][0]["inputSchema"]["type"], "object");
assert_eq!(tools["tools"][0]["outputSchema"]["type"], "string");
}
#[tokio::test]
async fn initialize_and_resources_expose_server_card() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(
McpServerConfig::new(core)
.with_server_card(json!({"name": "fixture-card", "version": "1"})),
);
let session = SharedSession::new();
let init = server.handle_initialize(
json!(1),
&session,
&json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"clientInfo": {"name": "test", "version": "1"}
}),
);
assert_eq!(
init["result"]["serverInfo"]["card"]["name"],
json!("fixture-card")
);
assert!(init["result"]["capabilities"]["resources"].is_object());
let resources = server.resources_list_result(&json!({}));
assert_eq!(
resources["resources"][0]["uri"],
json!("well-known://mcp-card")
);
let read = server.handle_resources_read(json!(2), &json!({"uri": "well-known://mcp-card"}));
assert!(read["result"]["contents"][0]["text"]
.as_str()
.unwrap()
.contains("fixture-card"));
}
#[tokio::test]
async fn package_context_resources_templates_prompts_and_completions_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
dir.path().join("harn.toml"),
"[package]\nname = \"fixture\"\n",
)
.expect("write manifest");
std::fs::write(dir.path().join("README.md"), "# Fixture\n").expect("write readme");
std::fs::create_dir_all(dir.path().join("prompts")).expect("prompt dir");
std::fs::write(
dir.path().join("prompts/review.harn.prompt"),
r#"---
id = "review"
description = "Review code"
[[arguments]]
name = "language"
required = false
suggestions = ["rust", "ruby", "typescript"]
[[arguments]]
name = "code"
required = true
---
Review {{ language }}: {{ code }}
"#,
)
.expect("write prompt");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(McpServerConfig::new(core));
let session = SharedSession::new();
let init = server.handle_initialize(
json!(1),
&session,
&json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"clientInfo": {"name": "test", "version": "1"}
}),
);
assert!(init["result"]["capabilities"]["resources"].is_object());
assert!(init["result"]["capabilities"]["prompts"].is_object());
assert!(init["result"]["capabilities"]["completions"].is_object());
let resources = mcp_response(
&server,
harn_vm::jsonrpc::request(2, "resources/list", json!({})),
session.clone(),
)
.await;
let uris = resources["result"]["resources"]
.as_array()
.unwrap()
.iter()
.map(|resource| resource["uri"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(uris.contains(&"harn://package/manifest"));
assert!(uris.contains(&"harn://package/readme"));
assert!(uris.contains(&"harn://package/source"));
assert!(uris.contains(&"harn://prompt/review/source"));
let source = mcp_response(
&server,
harn_vm::jsonrpc::request(3, "resources/read", json!({"uri": "harn://package/source"})),
session.clone(),
)
.await;
assert!(source["result"]["contents"][0]["text"]
.as_str()
.unwrap()
.contains("pub fn greet"));
let templates = mcp_response(
&server,
harn_vm::jsonrpc::request(4, "resources/templates/list", json!({})),
session.clone(),
)
.await;
assert_eq!(
templates["result"]["resourceTemplates"][0]["uriTemplate"],
json!("harn://package/{artifact}")
);
assert_eq!(
templates["result"]["resourceTemplates"][1]["uriTemplate"],
json!("harn://prompt/{name}/source")
);
let prompts = mcp_response(
&server,
harn_vm::jsonrpc::request(5, "prompts/list", json!({})),
session.clone(),
)
.await;
assert_eq!(prompts["result"]["prompts"][0]["name"], json!("review"));
let prompt = mcp_response(
&server,
harn_vm::jsonrpc::request(
6,
"prompts/get",
json!({"name": "review", "arguments": {"language": "Rust", "code": "fn main() {}"}}),
),
session.clone(),
)
.await;
assert!(prompt["result"]["messages"][0]["content"]["text"]
.as_str()
.unwrap()
.contains("fn main"));
let prompt_completion = mcp_response(
&server,
harn_vm::jsonrpc::request(
7,
mcp_protocol::METHOD_COMPLETION_COMPLETE,
json!({
"ref": {"type": "ref/prompt", "name": "review"},
"argument": {"name": "language", "value": "ru"}
}),
),
session.clone(),
)
.await;
assert_eq!(
prompt_completion["result"]["completion"]["values"],
json!(["ruby", "rust"])
);
let resource_completion = mcp_response(
&server,
harn_vm::jsonrpc::request(
8,
mcp_protocol::METHOD_COMPLETION_COMPLETE,
json!({
"ref": {"type": "ref/resource", "uri": "harn://package/{artifact}"},
"argument": {"name": "artifact", "value": "ma"}
}),
),
session,
)
.await;
assert_eq!(
resource_completion["result"]["completion"]["values"],
json!(["manifest"])
);
}
#[tokio::test]
async fn protocol_context_requires_configured_auth() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let mut config = DispatchCoreConfig::for_script(&script);
config.auth_policy = crate::AuthPolicy {
methods: vec![AuthMethodConfig::ApiKey(ApiKeyAuthConfig::single("secret"))],
mcp_allowlist: None,
};
let core = DispatchCore::new(config).expect("core");
let server = McpServer::new(McpServerConfig::new(core));
let session = SharedSession::new();
let _ = server.handle_initialize(
json!(1),
&session,
&json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"clientInfo": {"name": "test", "version": "1"}
}),
);
let unauthorized = mcp_response(
&server,
harn_vm::jsonrpc::request(2, "resources/list", json!({})),
session.clone(),
)
.await;
assert_eq!(unauthorized["error"]["code"], json!(-32001));
let authorized = match server
.process_message(
harn_vm::jsonrpc::request(3, "resources/list", json!({})),
session,
AuthRequest {
headers: BTreeMap::from([(
"authorization".to_string(),
"Bearer secret".to_string(),
)]),
..AuthRequest::default()
},
)
.await
{
ImmediateResult::Response(response) => response,
ImmediateResult::Accepted | ImmediateResult::Stream(_) => {
panic!("expected auth response")
}
};
assert!(authorized["result"]["resources"].is_array());
}
#[tokio::test]
async fn sampling_and_elicitation_requests_return_boundary_errors() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(McpServerConfig::new(core));
let session = SharedSession::new();
let _ = server.handle_initialize(
json!(1),
&session,
&json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"clientInfo": {"name": "test", "version": "1"}
}),
);
for (method, feature) in [
(mcp_protocol::METHOD_SAMPLING_CREATE_MESSAGE, "sampling"),
(mcp_protocol::METHOD_ELICITATION_CREATE, "elicitation"),
] {
let response = mcp_response(
&server,
harn_vm::jsonrpc::request(2, method, json!({})),
session.clone(),
)
.await;
assert_eq!(response["error"]["code"], json!(-32601));
assert_eq!(response["error"]["data"]["feature"], json!(feature));
assert_eq!(response["error"]["data"]["role"], json!("client"));
}
}
#[tokio::test]
async fn adapter_protocol_fixture_matches_checked_in_matrix() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(
McpServerConfig::new(core)
.with_server_card(json!({"name": "fixture-card", "version": "1"})),
);
let session = SharedSession::new();
let initialize = harn_vm::jsonrpc::request(
1,
"initialize",
json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "fixture-client", "version": "1"}
}),
);
let tools_list = harn_vm::jsonrpc::request(2, "tools/list", json!({}));
let resources_list = harn_vm::jsonrpc::request(3, "resources/list", json!({}));
let resources_read =
harn_vm::jsonrpc::request(4, "resources/read", json!({"uri": "well-known://mcp-card"}));
let templates_list = harn_vm::jsonrpc::request(5, "resources/templates/list", json!({}));
let actual = vec![
initialize.clone(),
mcp_response(&server, initialize, session.clone()).await,
harn_vm::jsonrpc::notification("notifications/initialized", json!({})),
tools_list.clone(),
mcp_response(&server, tools_list, session.clone()).await,
resources_list.clone(),
mcp_response(&server, resources_list, session.clone()).await,
resources_read.clone(),
mcp_response(&server, resources_read, session.clone()).await,
templates_list.clone(),
mcp_response(&server, templates_list, session).await,
];
crate::protocol_fixture_tests::assert_fixture_documents_match(
"conformance/protocols/fixtures/mcp/adapter_initialize_tools_resources.valid.json",
actual,
);
}
async fn mcp_response(server: &McpServer, request: JsonValue, session: SharedSession) -> JsonValue {
match server
.process_message(request, session, AuthRequest::default())
.await
{
ImmediateResult::Response(response) => response,
ImmediateResult::Accepted | ImmediateResult::Stream(_) => {
panic!("expected MCP JSON-RPC response")
}
}
}
#[tokio::test]
async fn tool_call_rejects_task_augmentation() {
let dir = tempfile::tempdir().expect("tempdir");
let script = dir.path().join("server.harn");
std::fs::write(
&script,
r"
pub fn greet(name: string) -> string {
return name
}
",
)
.expect("write script");
let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
let server = McpServer::new(McpServerConfig::new(core));
let session = SharedSession::new();
let _ = server.handle_initialize(
json!(1),
&session,
&json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"clientInfo": {"name": "test", "version": "1"}
}),
);
let response = match server
.process_message(
harn_vm::jsonrpc::request(
2,
"tools/call",
json!({
"name": "greet",
"arguments": {"name": "alice"},
"task": {"title": "async please"}
}),
),
session,
AuthRequest::default(),
)
.await
{
ImmediateResult::Response(response) => response,
ImmediateResult::Accepted | ImmediateResult::Stream(_) => {
panic!("expected task-augmentation error response")
}
};
assert_eq!(response["error"]["code"], json!(-32602));
assert_eq!(response["error"]["data"]["feature"], json!("tasks"));
}
#[test]
fn paged_result_returns_next_cursor_and_decodes_it() {
let entries = (0..55)
.map(|index| json!({"name": format!("tool-{index}")}))
.collect::<Vec<_>>();
let first = paged_result("tools", entries.clone(), &json!({}));
assert_eq!(first["tools"].as_array().unwrap().len(), 50);
assert_eq!(first["tools"][49]["name"], json!("tool-49"));
let second = paged_result(
"tools",
entries,
&json!({"cursor": first["nextCursor"].as_str().unwrap()}),
);
assert_eq!(second["tools"].as_array().unwrap().len(), 5);
assert_eq!(second["tools"][0]["name"], json!("tool-50"));
assert!(second.get("nextCursor").is_none());
}
#[test]
fn build_call_request_accepts_named_arguments() {
let request = build_call_request(
"mcp",
"tester",
"greet",
json!({"name": "alice"}),
AuthRequest::default(),
Arc::new(AtomicBool::new(false)),
None,
None,
)
.expect("call request");
match request.arguments {
CallArguments::Named(values) => assert_eq!(values["name"], json!("alice")),
other => panic!("expected named arguments, got {other:?}"),
}
}
#[test]
fn streamable_http_accept_negotiation_uses_sse_only_when_json_is_absent() {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
assert!(should_stream_post_response(&headers));
headers.insert(
ACCEPT,
HeaderValue::from_static("application/json, text/event-stream"),
);
assert!(!should_stream_post_response(&headers));
}
#[test]
fn legacy_deprecation_header_is_attached() {
let mut response = StatusCode::ACCEPTED.into_response();
attach_legacy_deprecation_headers(&mut response);
assert_eq!(
response
.headers()
.get(DEPRECATION_HEADER)
.and_then(|value| value.to_str().ok()),
Some("true")
);
}