Metadata-Version: 2.3
Name: kairos-core-mcp
Version: 0.1.1
Summary: Shared MCP server for Kairos agents, run over stdio as a subprocess
Author: Denys
Author-email: Denys <denys.bondarchuk@kairoshelix.ai>
Requires-Dist: fastmcp>=3.4.7
Requires-Python: >=3.13
Description-Content-Type: text/markdown

# kairos-core-mcp

The shared MCP server for Kairos agents: one process, spawned by the agent itself,
holding the tools that more than one agent needs — and a registration decorator that
makes adding those tools a one-liner.

## What MCP is

The Model Context Protocol lets a model call server-side functions. A server publishes
**tools** — named functions with a JSON Schema for their arguments — and a client
discovers them at connect time and calls them by name.

Two consequences shape this server:

- **The agent owns the process.** There is no endpoint and nothing to deploy: the
  client launches `kairos-core-mcp` as a child process and speaks MCP over its stdin
  and stdout. The server's lifetime is the agent's lifetime, and its capabilities are
  whatever the installed version registered.
- **stdout belongs to the protocol.** It carries the JSON-RPC stream, so a `print`
  inside a tool corrupts the session. Write diagnostics to stderr.

## Tools

None ship yet. This package is where the shared ones land — a tool that two agents
would otherwise each own a copy of belongs here, and every agent gets it by upgrading
the dependency.

**The contract of a tool is its docstring.** It is published with the schema, so the
inspector and any client show it verbatim — read it there rather than here, and keep
it correct when changing behaviour.

## Registering a tool

```python
from kairos_core_mcp.tools import tool


@tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b
```

That is the whole registration. `tool()` wraps FastMCP's own standalone decorator and
passes every keyword through untouched — `name`, `title`, `description`, `tags`,
`meta`, `output_schema`, `annotations`, `timeout`, `exclude_args` — so a tool is
described exactly as the FastMCP documentation describes it, and the object you get
back is the function you wrote. Nothing is reimplemented; the schema, the coercion and
the result envelope stay FastMCP's.

What the wrapper adds is the registry. The decorated tool is appended to a
module-level list, and `main()` walks that list into `mcp.add_tool()` at startup:

```python
for tool in get_tools():
    mcp.add_tool(tool)
```

So a tool is declared in exactly one place. There is no server object to import at
definition time, no manual list to keep in sync, and no way to write a `@tool` that
the process then fails to serve. Passing metadata through works the same way — the
client sees it on the published tool:

```python
@tool(
    meta={
        "artifact_bindings": {
            "artifact_ref": {
                "targets": ["columns", "rows"],
                "description": "Reference to an earlier artifact.",
            }
        }
    }
)
def predict(columns: list[str], rows: list[list[int]]) -> int:
    """Run the model over a table given as columns and rows."""
    return len(rows)
```

### Where a tool lives

`src/kairos_core_mcp/tools/<name>/tool.py`, re-exported from
`src/kairos_core_mcp/tools/__init__.py`:

```python
from .add.tool import add
from .decorator import tool

__all__ = ["add", "tool"]
```

**A decorator only runs when its module is imported.** The registry is filled as an
import side effect, and `main()` imports the `tools` package — so the re-export above
is what makes a tool real. A module nothing imports registers nothing, silently.

## Running

Normally the agent runs it, as a child process over stdio:

```python
from fastmcp import Client
from fastmcp.client.transports import StdioTransport

transport = StdioTransport(command="kairos-core-mcp", args=[])

async with Client(transport) as client:
    tools = await client.list_tools()
```

The command must be resolvable by the agent's own process — the console script from
this package's install, or `uv run --directory /path/to/kairos-core-mcp
kairos-core-mcp` when the agent runs from elsewhere. The equivalent in a client that
takes a config file:

```json
{
  "mcpServers": {
    "kairos-core": {
      "command": "kairos-core-mcp",
      "args": []
    }
  }
}
```

You can also start it yourself. With uv:

```bash
uv sync
uv run kairos-core-mcp
```

With plain Python — installing the package puts the CLI on your PATH:

```bash
python -m venv .venv
source .venv/bin/activate           # Windows: .venv\Scripts\activate
pip install -e .
kairos-core-mcp
```

Either way it reads JSON-RPC from stdin and answers on stdout, so by hand it just sits
there waiting for a client. Which is what the inspector is for.

## Inspector

A browser UI to list the tools and call them by hand. Unlike an HTTP server, there is
nothing to start first — the inspector spawns the process itself, so give it the
command:

```bash
npx @modelcontextprotocol/inspector uv run kairos-core-mcp
```

With the package installed and the CLI on your PATH:

```bash
npx @modelcontextprotocol/inspector kairos-core-mcp
```

Each tool appears with a form built from its input schema. Fill it, run it, and see
both the rendered `content` and the raw `structuredContent` of the reply. This is the
fastest check after adding a tool: if it is missing from the list, the module is not
imported.

## Tests

```bash
uv run pytest
```

The registry is process-global, so a fixture clears it around every test — build a
throwaway `FastMCP` from `get_tools()` and call the tool through a `Client`, the way
`tests/tools/test_decorator.py` does. That exercises the real schema and the real
dispatch rather than the Python function alone.
