feat: add agent publishing foundation

This commit is contained in:
a.tolmachev
2026-03-29 22:10:15 +03:00
parent d757adb192
commit 7b7699cf8b
18 changed files with 1520 additions and 105 deletions
+186 -21
View File
@@ -51,12 +51,14 @@ mod tests {
use axum::{Json, Router, http::header, routing::post};
use crank_adapter_grpc::test_support as grpc_test_support;
use crank_core::{
DescriptorId, ExecutionConfig, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod,
Operation, OperationId, OperationStatus, Protocol, RestTarget, Target, ToolDescription,
WorkspaceId,
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, DescriptorId,
ExecutionConfig, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod, Operation,
OperationId, OperationStatus, Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_registry::{PostgresRegistry, PublishRequest};
use crank_registry::{
CreateAgentRequest, PostgresRegistry, PublishAgentRequest, PublishRequest,
};
use crank_schema::{Schema, SchemaKind};
use serde_json::{Value, json};
use sqlx::{Executor, postgres::PgPoolOptions};
@@ -68,6 +70,10 @@ mod tests {
WorkspaceId::new("ws_default")
}
fn test_workspace_slug() -> &'static str {
"default"
}
#[tokio::test]
async fn initializes_lists_and_calls_published_tool_via_mcp() {
let registry = test_registry().await;
@@ -88,6 +94,7 @@ mod tests {
})
.await
.unwrap();
publish_agent_for_operation(&registry, &operation, "sales-rest").await;
let base_url = spawn_mcp_server(build_app(
registry,
@@ -96,11 +103,12 @@ mod tests {
))
.await;
let client = reqwest::Client::new();
let initialized_session = initialize_session(&client, &base_url).await;
let mcp_url = agent_mcp_url(&base_url, "sales-rest");
let initialized_session = initialize_session(&client, &mcp_url).await;
let tools = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -112,7 +120,7 @@ mod tests {
.await;
let call_result = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -156,6 +164,7 @@ mod tests {
})
.await
.unwrap();
publish_agent_for_operation(&registry, &operation, "sales-graphql").await;
let base_url = spawn_mcp_server(build_app(
registry,
@@ -164,11 +173,12 @@ mod tests {
))
.await;
let client = reqwest::Client::new();
let initialized_session = initialize_session(&client, &base_url).await;
let mcp_url = agent_mcp_url(&base_url, "sales-graphql");
let initialized_session = initialize_session(&client, &mcp_url).await;
let call_result = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -211,6 +221,7 @@ mod tests {
})
.await
.unwrap();
publish_agent_for_operation(&registry, &operation, "sales-grpc").await;
let base_url = spawn_mcp_server(build_app(
registry,
@@ -219,11 +230,12 @@ mod tests {
))
.await;
let client = reqwest::Client::new();
let initialized_session = initialize_session(&client, &base_url).await;
let mcp_url = agent_mcp_url(&base_url, "sales-grpc");
let initialized_session = initialize_session(&client, &mcp_url).await;
let call_result = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -256,8 +268,9 @@ mod tests {
))
.await;
let client = reqwest::Client::new();
let mcp_url = agent_mcp_url(&base_url, "sales-init");
let initialize_response = client
.post(format!("{base_url}/mcp"))
.post(&mcp_url)
.header(header::ACCEPT, "application/json, text/event-stream")
.json(&json!({
"jsonrpc": "2.0",
@@ -278,7 +291,7 @@ mod tests {
.unwrap()
.to_owned();
let tools_list = client
.post(format!("{base_url}/mcp"))
.post(&mcp_url)
.header(header::ACCEPT, "application/json, text/event-stream")
.header("MCP-Session-Id", session_id)
.header("MCP-Protocol-Version", "2025-11-25")
@@ -309,11 +322,12 @@ mod tests {
))
.await;
let client = reqwest::Client::new();
let initialized_session = initialize_session(&client, &base_url).await;
let mcp_url = agent_mcp_url(&base_url, "sales-refresh");
let initialized_session = initialize_session(&client, &mcp_url).await;
let before_publish = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -339,10 +353,11 @@ mod tests {
})
.await
.unwrap();
publish_agent_for_operation(&registry, &operation, "sales-refresh").await;
let after_publish = post_jsonrpc(
&client,
&base_url,
&mcp_url,
Some(&initialized_session),
json!({
"jsonrpc": "2.0",
@@ -360,9 +375,87 @@ mod tests {
);
}
async fn initialize_session(client: &reqwest::Client, base_url: &str) -> String {
#[tokio::test]
async fn lists_only_bound_tools_for_agent_context() {
let registry = test_registry().await;
let upstream_base_url = spawn_upstream_server().await;
let operation_a = test_operation(&upstream_base_url, "crm_create_lead");
let operation_b = test_operation(&upstream_base_url, "crm_update_lead");
for operation in [&operation_a, &operation_b] {
registry
.create_operation(&test_workspace_id(), operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
workspace_id: &test_workspace_id(),
operation_id: &operation.id,
version: 1,
published_at: "2026-03-26T10:00:00Z",
published_by: Some("alice"),
})
.await
.unwrap();
}
publish_agent_with_bindings(
&registry,
"sales-a",
vec![binding_for_operation(&operation_a)],
)
.await;
publish_agent_with_bindings(
&registry,
"sales-b",
vec![binding_for_operation(&operation_b)],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-b");
let session_a = initialize_session(&client, &agent_a_url).await;
let session_b = initialize_session(&client, &agent_b_url).await;
let tools_a = post_jsonrpc(
&client,
&agent_a_url,
Some(&session_a),
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}),
)
.await;
let tools_b = post_jsonrpc(
&client,
&agent_b_url,
Some(&session_b),
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}),
)
.await;
assert_eq!(tools_a["result"]["tools"][0]["name"], "crm_create_lead");
assert_eq!(tools_b["result"]["tools"][0]["name"], "crm_update_lead");
}
async fn initialize_session(client: &reqwest::Client, mcp_url: &str) -> String {
let initialize_response = client
.post(format!("{base_url}/mcp"))
.post(mcp_url)
.header(header::ACCEPT, "application/json, text/event-stream")
.json(&json!({
"jsonrpc": "2.0",
@@ -384,7 +477,7 @@ mod tests {
.to_owned();
let initialized_response = client
.post(format!("{base_url}/mcp"))
.post(mcp_url)
.header(header::ACCEPT, "application/json, text/event-stream")
.header("MCP-Session-Id", &session_id)
.header("MCP-Protocol-Version", "2025-11-25")
@@ -404,12 +497,12 @@ mod tests {
async fn post_jsonrpc(
client: &reqwest::Client,
base_url: &str,
mcp_url: &str,
session_id: Option<&str>,
payload: Value,
) -> Value {
let mut request = client
.post(format!("{base_url}/mcp"))
.post(mcp_url)
.header(header::ACCEPT, "application/json, text/event-stream")
.header("MCP-Protocol-Version", "2025-11-25");
@@ -462,6 +555,78 @@ mod tests {
format!("http://{}", address)
}
fn agent_mcp_url(base_url: &str, agent_slug: &str) -> String {
format!("{base_url}/v1/{}/{}", test_workspace_slug(), agent_slug)
}
async fn publish_agent_for_operation(
registry: &PostgresRegistry,
operation: &Operation<Schema, MappingSet>,
agent_slug: &str,
) {
publish_agent_with_bindings(registry, agent_slug, vec![binding_for_operation(operation)])
.await;
}
async fn publish_agent_with_bindings(
registry: &PostgresRegistry,
agent_slug: &str,
bindings: Vec<AgentOperationBinding>,
) {
let agent_id = AgentId::new(format!("agent_{agent_slug}"));
let agent = Agent {
id: agent_id.clone(),
workspace_id: test_workspace_id(),
slug: agent_slug.to_owned(),
display_name: format!("Agent {agent_slug}"),
description: "Curated MCP toolset".to_owned(),
status: AgentStatus::Draft,
current_draft_version: 1,
latest_published_version: None,
created_at: "2026-03-26T10:00:00Z".to_owned(),
updated_at: "2026-03-26T10:00:00Z".to_owned(),
published_at: None,
};
let version = AgentVersion {
agent_id: agent_id.clone(),
version: 1,
status: AgentStatus::Draft,
instructions: json!({}),
tool_selection_policy: json!({}),
created_at: "2026-03-26T10:00:00Z".to_owned(),
};
registry
.create_agent(CreateAgentRequest {
agent: &agent,
version: &version,
bindings: &bindings,
})
.await
.unwrap();
registry
.publish_agent(PublishAgentRequest {
workspace_id: &test_workspace_id(),
agent_id: &agent_id,
version: 1,
published_at: "2026-03-26T10:00:00Z",
published_by: Some("alice"),
})
.await
.unwrap();
}
fn binding_for_operation(operation: &Operation<Schema, MappingSet>) -> AgentOperationBinding {
AgentOperationBinding {
operation_id: operation.id.clone(),
operation_version: 1,
tool_name: operation.name.clone(),
tool_title: operation.tool_description.title.clone(),
tool_description_override: None,
enabled: true,
}
}
async fn create_lead(Json(payload): Json<Value>) -> Json<Value> {
Json(json!({
"id": "lead_123",