# mcp


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## The server

[`server`](https://vedicreader.github.io/ramabana/mcp.html#server)
builds the MCP server. Pass a host to serve its tools. Pass an agent as
well and the server mounts the agent’s own tools, the same objects a
turn gets, so a client’s calls appear in the same activity feed and
`changes()` report as a turn’s.
[`Agent._record`](https://vedicreader.github.io/ramabana/agent.html#agent._record)
wraps each tool with `functools.wraps`, which keeps its signature and
docstring, and FastMCP builds each tool’s schema from those. A tool is
therefore described in one place, and the description a client sees
cannot drift from the one a model sees. Without an agent, the server
builds the tools from the host with
[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for).

`readonly` defaults to True because the client is another agent, and
this server cannot put an approval prompt in front of the client’s user.
Read-only leaves out every write tool. The read tools stay, and so do
acting tools such as `inspect_python` and `generate_image`.
`readonly=False` mounts the write tools too, and then the client’s own
approval settings are what stands in front of them. `delegate=False`
leaves out `ask` even when an agent is given, and any other keyword goes
to `FastMCP`.

`INSTRUCTIONS` is the text the server sends a client when it connects.
It tells the client’s model how to read and edit through these tools.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/mcp.py#L43"
target="_blank" style="float:right; font-size:smaller">source</a>

### server

``` python
def server(
    host:NoneType=None, agent:NoneType=None, name:str='ramabana', readonly:bool=True, delegate:bool=True, **kw
):
```

*An MCP server over one host’s tools, and optionally over Ramabana’s own
agent.*

Below is a server over
[`FullHost`](https://vedicreader.github.io/ramabana/testing.html#fullhost),
a test host from [testing](04_testing.ipynb) that declares the code,
file, git, memory, notebook, session, shell and web groups. The server
is read-only by default, so `replace_text` and `create_file` are absent.
Without an agent it also mounts shalya’s `legacy` group (`list_files`,
`list_vars` and others) for one release, so that a client written
against version 0.1 keeps working. `edit_file` belongs to the opt-in
`exhash` group and is not mounted.

``` python
host = FullHost(files={'pkg/sizes.py': 'RESERVE = 16_384\n\ndef threshold(ctx):\n    return ctx - RESERVE\n'})
mcp = server(host)
tools = await mcp.list_tools()
[t.name for t in tools]
```

``` python
test_eq(set(t.name for t in tools) & WRITE_TOOLS, set())
test_eq('view_file' in [t.name for t in tools], True)
assert 'list_files' in [t.name for t in tools] and 'edit_file' not in [t.name for t in tools]
len(tools)
```

Each tool arrives with the schema FastMCP derived from the function
itself. The description is the docstring a model reads in a turn, and
the parameters come from the type annotations.

``` python
t = next(t for t in tools if t.name == 'view_file')
t.description.splitlines()[0], t.inputSchema['properties'], t.annotations.readOnlyHint
```

MCP tool annotations tell a client which calls only read, which are
destructive, and which reach the open world, meaning the web and API
tools.
[`_annotate`](https://vedicreader.github.io/ramabana/mcp.html#_annotate)
sets them from the same catalog entry that approvals and the activity
feed use. A client can use them to decide which calls to ask its user
about. This server is built with `readonly=False`, so `replace_text` is
there to compare.

``` python
mcp_w = server(host, readonly=False)
{t.name: (t.annotations.readOnlyHint, t.annotations.destructiveHint, t.annotations.openWorldHint)
 for t in await mcp_w.list_tools() if t.name in ('view_file', 'replace_text', 'web_search')}
```

Calling a tool goes through the same code path as a turn, and the client
gets back exactly the text the model would get. `view_file` returns one
`lineno|hash|content` line per line of the file.

``` python
out = await mcp.call_tool('view_file', {'path': 'pkg/sizes.py'})
print(out[0][0].text if isinstance(out, tuple) else out)
```

A write tool left out of a read-only server is absent from the MCP
surface, and calling its name returns an error. The last line lists the
write tools that `readonly=False` mounts.

``` python
with expect_fail(contains='Unknown tool'): await mcp.call_tool('replace_text', {'path': 'pkg/sizes.py', 'edits': []})
with expect_fail(contains='Unknown tool'): await mcp.call_tool('edit_file', {'path': 'pkg/sizes.py', 'commands': []})
[t.name for t in await mcp_w.list_tools() if t.name in WRITE_TOOLS]
```

## Skills as resources

Skills are offered as resources, not tools. A resource is text a client
can read, where a tool is a call that may have an effect. The server
publishes three kinds:

- `ramabana://skills`, an index of every skill with its one-line
  description of when it applies
- `skill://{name}`, one skill in full, the same text `read_skill`
  returns in a turn
- `ramabana://status`, a JSON summary of the open folders, the mounted
  tools, the skills and, with an agent, the agent’s status

A client reads the index and fetches only the skill it needs, the same
saving `read_skill` gives a model in a turn. Without an agent, the
server finds skills in the served folders and in `~/.agents/skills`.

``` python
[str(r.uriTemplate) for r in await mcp.list_resource_templates()] + [str(r.uri) for r in await mcp.list_resources()]
```

``` python
print((await mcp.read_resource('ramabana://status'))[0].content[:200])
```

## The agent as one tool

With an agent passed in, the server also offers `ask`, which runs a
whole Ramabana turn behind one call. The client sends one task and gets
back one answer. The tool loop, the tool results and any compaction of
the history happen on this side, and the client never sees them. Below,
[`fake_agent`](https://vedicreader.github.io/ramabana/testing.html#fake_agent)
from [testing](04_testing.ipynb) supplies an agent with a scripted
reply, so the example needs no model.

``` python
agent, be = fake_agent(replies=['`threshold` is defined in pkg/sizes.py, and subtracts the reserve.'])
full = server(agent.host, agent)
[t.name for t in await full.list_tools() if t.name == 'ask']
```

``` python
out = await full.call_tool('ask', {'task': 'where is the threshold defined?'})
out[0][0].text if isinstance(out, tuple) else out
```

`ask` runs the turn on a worker thread, because a turn blocks and an MCP
server is asynchronous. The client’s one call sent the agent’s backend
one message.

``` python
test_eq(len(be.sent), 1)
(await full.read_resource('ramabana://status'))[0].content[:60]
```

## Running it

`ramabana-mcp` is the command-line entry point,
[`main`](https://vedicreader.github.io/ramabana/cli.html#main). It
splits `--root` into folders and serves them over `--transport`, which
is `stdio` by default. `--model` builds a full agent on that model and
turns on the `ask` tool. Without it there is nothing to hand a task to,
so the server offers tools only. `--write` mounts the write tools, and
the other options build the host the way the terminal does.

The agent behind `ask` is built with `approve='none'`, which means no
approval gate at all, so the turn `ask` runs may use the agent’s write
tools without asking anyone. `--write` decides only which tools the
client may call directly.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/cli.py#L2411"
target="_blank" style="float:right; font-size:smaller">source</a>

### main

``` python
def main(
    root:str='.', # comma-separated folders to serve
    model:str=None, # model for `ask`; omit to serve tools only
    write:bool=False, # mount the write tools too
    web:bool=True, # let web tools reach the network via fossick
    read_outside:bool=False, # reads may name any path; writes stay inside
    vault:bool=False, # keep reads in a vishalakshi vault
    pii:str='off', # off | redact | refuse PII from the vault
    pii_ner:bool=False, # --pii also gates titled names, not only patterns
    transport:str='stdio', # stdio | sse | streamable-http
    cfg:str=None, # config dir, for skills and extensions
):
```

*Serve Ramabana tools over MCP.*

A client that launches its own servers takes the command and its
arguments. Over a repository, with `ask` running on a local model, that
is:

``` json
{
  "mcpServers": {
    "ramabana": {
      "command": "ramabana-mcp",
      "args": ["--root", "/path/to/repo", "--model", "qwen-4b"]
    }
  }
}
```

Start with the default, read-only tools and no model:

``` bash
ramabana-mcp --root .
```
