Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c47bd4e
feat(cortex): MCP stdio server scaffold behind --mcp flag
ApiliumDevTeam Jun 22, 2026
0591621
fix(cortex): error out when --mcp used without the mcp feature
ApiliumDevTeam Jun 22, 2026
1739869
feat(cortex): MCP error conversion layer
ApiliumDevTeam Jun 22, 2026
a1a0779
feat(cortex): service layer + aingle_query_pattern MCP tool
ApiliumDevTeam Jun 22, 2026
e6da8b8
refactor(cortex): tidy MCP tool template + add query round-trip test
ApiliumDevTeam Jun 22, 2026
7e96311
feat(cortex): aingle_graph_stats MCP tool
ApiliumDevTeam Jun 22, 2026
0d9cf69
feat(cortex): aingle_create_triple MCP tool (write)
ApiliumDevTeam Jun 22, 2026
df82eb7
fix(cortex): correct aingle_create_triple idempotency hint + docs
ApiliumDevTeam Jun 22, 2026
3957304
feat(cortex): aingle_dag_history MCP tool (provenance)
ApiliumDevTeam Jun 22, 2026
1256339
feat(cortex): aingle_verify_proof MCP tool
ApiliumDevTeam Jun 22, 2026
1a69bc7
test(cortex): MCP integration + stdout hygiene
ApiliumDevTeam Jun 22, 2026
8d34400
docs: MCP server usage and client config
ApiliumDevTeam Jun 22, 2026
665b966
style(cortex): rustfmt mcp feature files
ApiliumDevTeam Jun 22, 2026
5428cd5
fix(cortex): compile MCP without dag feature; mark read tools read-only
ApiliumDevTeam Jun 22, 2026
1be3812
feat(cortex): MCP triple tools (batch_insert, get, delete, list)
ApiliumDevTeam Jun 22, 2026
0d44816
feat(cortex): MCP query tools (list_subjects, list_predicates)
ApiliumDevTeam Jun 22, 2026
bda4010
feat(cortex): MCP aingle_sparql tool (sparql-gated)
ApiliumDevTeam Jun 22, 2026
1cb8f73
feat(cortex): MCP DAG tools (tips, action, chain, stats, prune)
ApiliumDevTeam Jun 22, 2026
fedf158
feat(cortex): MCP aingle_get_proof tool
ApiliumDevTeam Jun 22, 2026
5eee4c9
feat(cortex): MCP skill tools (validate, sandbox create/delete)
ApiliumDevTeam Jun 22, 2026
43457f6
feat(cortex): MCP agent/reasoning tools (consistency, verify_assertio…
ApiliumDevTeam Jun 22, 2026
512e0f1
style(cortex): gate MCP service imports + rustfmt for full feature ma…
ApiliumDevTeam Jun 22, 2026
25915b1
Merge remote-tracking branch 'origin/main' into worktree-feat+aingle-…
ApiliumDevTeam Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,46 @@ aingle-minimal run --rest-port 19080

---

## MCP Server

The Cortex exposes the AIngle semantic graph to MCP clients like Claude Code and Claude Desktop over the Model Context Protocol (stdio), letting agents query, write, and verify graph data through tool calls.

### Build

```bash
cargo build -p aingle_cortex --features "mcp dag" --release
```

### Client configuration

Add to `claude_desktop_config.json` (Claude Desktop) or `.mcp.json` (Claude Code):

```json
{
"mcpServers": {
"aingle": {
"command": "aingle-cortex",
"args": ["--mcp", "--db", "./data/graph.sled"]
}
}
}
```

Replace `--db <path>` with `--memory` for an ephemeral, in-memory graph.

### Available tools

- `aingle_ping` — liveness check
- `aingle_query_pattern` — query the semantic graph by triple pattern
- `aingle_graph_stats` — graph statistics
- `aingle_create_triple` — insert a triple (write)
- `aingle_verify_proof` — verify a zero-knowledge proof (returns `valid: false` for invalid proofs)
- `aingle_dag_history` — signed DAG provenance history of a subject (requires the `dag` feature)

> stdout is reserved for the JSON-RPC stream; logs are written to stderr.

---

## Contributing

We welcome contributions from the community.
Expand Down
8 changes: 8 additions & 0 deletions crates/aingle_cortex/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex"]
p2p-mdns = ["p2p", "dep:mdns-sd", "dep:if-addrs"]
cluster = ["p2p", "dep:aingle_wal", "dep:aingle_raft", "dep:openraft", "dep:tokio-rustls", "dep:rustls-pemfile"]
dag = ["cluster", "aingle_graph/dag", "aingle_graph/dag-sign", "aingle_raft/dag"]
mcp = ["dep:rmcp", "dep:schemars"]
full = ["rest", "graphql", "sparql", "auth", "dag"]

[[bin]]
Expand All @@ -47,6 +48,10 @@ async-graphql-axum = { version = "8.0.0-rc", optional = true }
# SPARQL (optional)
spargebra = { version = "0.4", optional = true }

# MCP server (optional) — Model Context Protocol over stdio
rmcp = { version = "1.7", features = ["server", "transport-io", "macros"], optional = true }
schemars = { version = "1.0", optional = true }

# Authentication (optional)
jsonwebtoken = { version = "10", features = ["rust_crypto"], optional = true }
argon2 = { version = "0.5", optional = true }
Expand Down Expand Up @@ -111,3 +116,6 @@ if-addrs = { version = "0.13", optional = true }
tempfile = "3.26"
reqwest = { version = "0.12", features = ["json"] }
tokio-test = "0.4"
# Enable the rmcp `client` feature for the in-process MCP integration test.
# This is dev-only; the production `mcp` feature uses the server side only.
rmcp = { version = "1.7", features = ["client", "transport-io"] }
13 changes: 8 additions & 5 deletions crates/aingle_cortex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,21 +164,24 @@
#[cfg(feature = "auth")]
pub mod auth;
pub mod client;
#[cfg(feature = "cluster")]
pub mod cluster_init;
pub mod error;
pub mod wasm_types;
#[cfg(feature = "graphql")]
pub mod graphql;
#[cfg(feature = "mcp")]
pub mod mcp;
pub mod middleware;
#[cfg(feature = "p2p")]
pub mod p2p;
pub mod proofs;
pub mod rest;
pub mod server;
pub mod service;
#[cfg(feature = "sparql")]
pub mod sparql;
pub mod state;
#[cfg(feature = "p2p")]
pub mod p2p;
#[cfg(feature = "cluster")]
pub mod cluster_init;
pub mod wasm_types;

pub use client::{CortexClientConfig, CortexInternalClient};
pub use error::{Error, Result};
Expand Down
42 changes: 36 additions & 6 deletions crates/aingle_cortex/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@
//! REST/GraphQL/SPARQL interface for AIngle semantic graphs.

use aingle_cortex::{CortexConfig, CortexServer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// In MCP mode, stdout is reserved for the JSON-RPC stream, so all logging
// must be redirected to stderr. Detect the flag before subscriber init.
let mcp_mode = std::env::args().any(|a| a == "--mcp");

// Initialize logging
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "aingle_cortex=info,tower_http=debug".into());
let fmt_layer = if mcp_mode {
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.boxed()
} else {
tracing_subscriber::fmt::layer().boxed()
};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "aingle_cortex=info,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(fmt_layer)
.init();

// Parse command line arguments
Expand Down Expand Up @@ -61,6 +71,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"--memory" => {
config.db_path = Some(":memory:".to_string());
}
"--mcp" => {
config.mcp_mode = true;
}
"--flush-interval" => {
if i + 1 < args.len() {
config.flush_interval_secs = args[i + 1].parse().unwrap_or(300);
Expand All @@ -76,6 +89,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
i += 1;
}

// If --mcp was requested but the binary was built without the `mcp` feature,
// fail loudly instead of silently falling through to the TCP REST server.
#[cfg(not(feature = "mcp"))]
if config.mcp_mode {
eprintln!("error: --mcp requires building with the `mcp` feature: cargo build -p aingle_cortex --features mcp");
std::process::exit(2);
}

// Parse P2P flags (feature-gated at compile time).
#[cfg(feature = "p2p")]
let p2p_config = {
Expand Down Expand Up @@ -159,6 +180,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
aingle_cortex::cluster_init::ensure_dag_ready(server.state_mut(), db_path.as_deref()).await;
}

// MCP mode: serve over stdio instead of binding a TCP listener.
#[cfg(feature = "mcp")]
if server.config().mcp_mode {
let state = server.state().clone();
aingle_cortex::mcp::serve_stdio(state).await?;
return Ok(());
}

// Spawn periodic flush task if enabled
if flush_interval_secs > 0 {
let flush_state = server.state().clone();
Expand Down Expand Up @@ -269,6 +298,7 @@ fn print_help() {
);
println!(" --memory Use volatile in-memory storage (no persistence)");
println!(" --flush-interval <S> Periodic flush interval in seconds (default: 300, 0=off)");
println!(" --mcp Serve MCP over stdio (requires --features mcp)");
println!(" -V, --version Print version and exit");
println!(" --help Print this help message");
println!();
Expand Down
35 changes: 35 additions & 0 deletions crates/aingle_cortex/src/mcp/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2019-2026 Apilium Technologies OÜ. All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR Commercial

//! Map cortex errors into MCP tool errors.

use crate::error::Error;
use rmcp::model::ErrorData as McpError;

/// Convert a cortex `Error` into an MCP error suitable for a failed tool result.
///
/// `InvalidInput` maps to the JSON-RPC `invalid_params` code; every other
/// variant falls through to `internal_error` carrying the error's display text.
pub fn to_mcp_error(err: Error) -> McpError {
match err {
Error::InvalidInput(msg) => McpError::invalid_params(msg, None),
other => McpError::internal_error(other.to_string(), None),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn invalid_input_maps_to_invalid_params() {
let e = to_mcp_error(Error::InvalidInput("bad".into()));
assert_eq!(e.message.as_ref(), "bad");
}

#[test]
fn other_maps_to_internal_error() {
let e = to_mcp_error(Error::Internal("boom".into()));
assert!(e.message.contains("boom"));
}
}
38 changes: 38 additions & 0 deletions crates/aingle_cortex/src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2019-2026 Apilium Technologies OÜ. All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR Commercial

//! Model Context Protocol (MCP) server for AIngle Córtex.
//!
//! Exposes the Córtex business-logic layer over MCP via a stdio transport,
//! so that MCP-capable clients (e.g. Claude Desktop, IDE agents) can interact
//! with AIngle semantic graphs as tools.
//!
//! stdout is reserved for the JSON-RPC stream; all logging must go to stderr.

mod convert;
mod server;

pub use server::AingleMcp;

use crate::state::AppState;

/// Serves the MCP server over stdio until the client disconnects.
///
/// stdout carries the JSON-RPC message stream; logging is expected to be
/// redirected to stderr by the caller before this is invoked.
pub async fn serve_stdio(state: AppState) -> crate::error::Result<()> {
use rmcp::transport::stdio;
use rmcp::ServiceExt;

let service = AingleMcp::new(state)
.serve(stdio())
.await
.map_err(|e| crate::error::Error::Internal(format!("MCP serve error: {e}")))?;

service
.waiting()
.await
.map_err(|e| crate::error::Error::Internal(format!("MCP wait error: {e}")))?;

Ok(())
}
Loading
Loading