feat: add agent publishing foundation
This commit is contained in:
+186
-21
@@ -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(®istry, &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(®istry, &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(®istry, &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(®istry, &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(
|
||||
®istry,
|
||||
"sales-a",
|
||||
vec![binding_for_operation(&operation_a)],
|
||||
)
|
||||
.await;
|
||||
publish_agent_with_bindings(
|
||||
®istry,
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user