feat: add graphql support

This commit is contained in:
a.tolmachev
2026-03-25 20:21:07 +03:00
parent 6296b04105
commit 1ea75eb824
16 changed files with 824 additions and 30 deletions
+1
View File
@@ -436,6 +436,7 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Schema(_) => "schema_validation_error",
RuntimeError::Mapping(_) => "mapping_error",
RuntimeError::GraphqlAdapter(_) => "adapter_execution_error",
RuntimeError::RestAdapter(_) => "adapter_execution_error",
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
RuntimeError::InvalidPreparedRequest { .. } => "runtime_error",
+148 -2
View File
@@ -50,8 +50,8 @@ mod tests {
use axum::{Json, Router, http::header, routing::post};
use mcpaas_core::{
ExecutionConfig, HttpMethod, Operation, OperationId, OperationStatus, Protocol, RestTarget,
Target, ToolDescription,
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, Operation, OperationId,
OperationStatus, Protocol, RestTarget, Target, ToolDescription,
};
use mcpaas_mapping::{MappingRule, MappingSet};
use mcpaas_registry::{PostgresRegistry, PublishRequest};
@@ -129,6 +129,60 @@ mod tests {
assert_eq!(call_result["result"]["isError"], false);
}
#[tokio::test]
async fn initializes_and_calls_published_graphql_tool_via_mcp() {
let registry = test_registry().await;
let endpoint = spawn_graphql_server().await;
let operation = test_graphql_operation(&endpoint, "crm_create_lead_graphql");
registry
.create_operation(&operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
operation_id: &operation.id,
version: 1,
published_at: "2026-03-26T10:00:00Z",
published_by: Some("alice"),
})
.await
.unwrap();
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://rmcp.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let initialized_session = initialize_session(&client, &base_url).await;
let call_result = post_jsonrpc(
&client,
&base_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "crm_create_lead_graphql",
"arguments": {
"email": "user@example.com"
}
}
}),
)
.await;
assert_eq!(
call_result["result"]["structuredContent"],
json!({ "id": "lead_123" })
);
assert_eq!(call_result["result"]["isError"], false);
}
#[tokio::test]
async fn requires_initialized_notification_before_tool_methods() {
let registry = test_registry().await;
@@ -321,6 +375,18 @@ mod tests {
format!("http://{}", address)
}
async fn spawn_graphql_server() -> String {
let app = Router::new().route("/", post(graphql_handler));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}", address)
}
async fn spawn_mcp_server(app: Router) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
@@ -339,6 +405,23 @@ mod tests {
}))
}
async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
let email = payload
.get("variables")
.and_then(|variables| variables.get("email"))
.and_then(Value::as_str)
.unwrap_or_default();
Json(json!({
"data": {
"createLead": {
"id": "lead_123",
"email": email
}
}
}))
}
async fn test_registry() -> PostgresRegistry {
let database_url = env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://rmcp:rmcp@127.0.0.1:5432/rmcp".to_owned());
@@ -426,6 +509,69 @@ mod tests {
}
}
fn test_graphql_operation(endpoint: &str, name: &str) -> Operation<Schema, MappingSet> {
Operation {
id: OperationId::new(format!("op_{name}")),
name: name.to_owned(),
display_name: "Create Lead GraphQL".to_owned(),
protocol: Protocol::Graphql,
status: OperationStatus::Published,
version: 1,
target: Target::Graphql(GraphqlTarget {
endpoint: endpoint.to_owned(),
operation_type: GraphqlOperationType::Mutation,
operation_name: "CreateLead".to_owned(),
query_template:
"mutation CreateLead($email: String!) { createLead(email: $email) { id email } }"
.to_owned(),
response_path: "$.response.body.data.createLead".to_owned(),
}),
input_schema: object_schema("email"),
output_schema: object_schema("id"),
input_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.mcp.email".to_owned(),
target: "$.request.variables.email".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
output_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.response.data.id".to_owned(),
target: "$.output.id".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
},
tool_description: ToolDescription {
title: "Create Lead GraphQL".to_owned(),
description: "Creates a CRM lead through GraphQL".to_owned(),
tags: vec!["graphql".to_owned()],
examples: Vec::new(),
},
samples: None,
generated_draft: None,
config_export: None,
created_at: "2026-03-26T10:00:00Z".to_owned(),
updated_at: "2026-03-26T10:00:00Z".to_owned(),
published_at: Some("2026-03-26T10:00:00Z".to_owned()),
}
}
fn object_schema(field_name: &str) -> Schema {
Schema {
kind: SchemaKind::Object,