Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 63 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A command-line interface for managing Model Context Interface (MCI) schemas and
- Connect your n8n, Make and other workflow builders as tools
- Convert any REST API Docs to AI tools in minute with LLM
- Run remote code with AWS Lambda, judge0, etc.
- Authentification, headers, body... Full set of API features are supported
- Authentication, headers, body... Full set of API features are supported
- CLI:
- Run server based CLI commands as tool from simple "ls" to anything else you can install with apt-get!
- Write separated python script and convert in tool in 30 seconds!
Expand All @@ -23,8 +23,8 @@ A command-line interface for managing Model Context Interface (MCI) schemas and
- Supports full templating as File type, but defined inside .mci.json
- Ideal for serving dynamic assets (image URLs per user, PDFs, etc)
- As well as for generating simple messages
- Make **toolset** from your custom tools: easiest way to orginize, manage and share your tools!
- Everything mantioned above you can use programatically via [MCI-Adapter](https://github.com/Model-Context-Interface/mci-py) for your language
- Make **toolset** from your custom tools: easiest way to organize, manage and share your tools!
- Everything mentioned above you can use programmatically via [MCI-Adapter](https://github.com/Model-Context-Interface/mci-py) for your language
- Or.. Instantly serve them as a unified **STDIO MCP server** via `uvx mcix run` command.
- And... Create separate .mci.json files to serve them as different MCP servers for different agents! Reducing token and runtime overhead by providing small, specific context files tailored per agent.

Expand Down Expand Up @@ -287,6 +287,66 @@ uvx mcix run

MCI tools support multiple execution types. Below are examples for each type:

## Tool Annotations

MCI tools support optional annotations that provide metadata and behavioral hints about the tool. These annotations are preserved when serving tools via MCP servers and help MCP clients make better decisions about tool usage and display.

### Supported Annotation Fields

All annotation fields are optional:

- **`title`**: Human-readable title for the tool (alternative to the machine name)
- **`readOnlyHint`**: `true` if the tool only reads data without modification, `false` if it modifies state
- **`destructiveHint`**: `true` if the tool may perform destructive updates (delete, overwrite), `false` if only additive
- **`idempotentHint`**: `true` if calling the tool repeatedly with the same arguments has no additional effect
- **`openWorldHint`**: `true` if the tool interacts with external entities (web APIs, databases), `false` for internal tools

**Example with annotations:**
```json
{
"name": "delete_resource",
"description": "Delete a resource from the remote server",
"annotations": {
"title": "Delete Resource",
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true
},
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "Resource ID"}
},
"required": ["id"]
},
"execution": {
"type": "http",
"method": "DELETE",
"url": "{{env.API_URL}}/resources/{{props.id}}"
}
}
```

**Example with partial annotations:**
```json
{
"name": "read_data",
"description": "Read data from the database",
"annotations": {
"title": "Read Data",
"readOnlyHint": true
},
"execution": {
"type": "http",
"method": "GET",
"url": "{{env.API_URL}}/data"
}
}
```

> **Note**: Annotations are automatically included when serving tools via `uvx mcix run`. MCP clients can use these annotations for filtering, validation, and user interface enhancements.

### Text Execution

Returns templated text using `{{props.field}}` and `{{env.VAR}}` syntax.
Expand Down
6 changes: 4 additions & 2 deletions mci.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
}
}
],
"toolsets": [],
"toolsets": [
"weather-tools"
],
"mcp_servers": {
"mci-docs": {
"url": "https://usemci.dev/mcp",
"type": "http"
}
}
}
}
4 changes: 3 additions & 1 deletion src/mci/core/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ class ServerInstance:
to the MCIClient instance.
"""

def __init__(self, server: Server, mci_client: MCIClient, env_vars: dict[str, str] | None = None):
def __init__(
self, server: Server, mci_client: MCIClient, env_vars: dict[str, str] | None = None
):
"""
Initialize the server instance.

Expand Down
43 changes: 40 additions & 3 deletions src/mci/core/tool_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def convert_to_mcp_tool(mci_tool: Tool) -> types.Tool:
Convert an MCI Tool to MCP Tool format.

Takes a Tool object from mci-py and converts it to the types.Tool format
expected by the MCP protocol. This includes converting the input schema
and preserving all metadata.
expected by the MCP protocol. This includes converting the input schema,
preserving all metadata, and transferring annotations.

Args:
mci_tool: Tool object from mci-py (Pydantic model)
Expand All @@ -43,11 +43,15 @@ def convert_to_mcp_tool(mci_tool: Tool) -> types.Tool:
# Convert inputSchema to MCP format (JSON Schema)
input_schema = MCIToolConverter.convert_input_schema(mci_tool.inputSchema or {})

# Create MCP Tool with converted schema
# Convert annotations to MCP format
annotations = MCIToolConverter.convert_annotations(mci_tool.annotations)

# Create MCP Tool with converted schema and annotations
return types.Tool(
name=mci_tool.name,
description=mci_tool.description or "",
inputSchema=input_schema,
annotations=annotations,
)

@staticmethod
Expand Down Expand Up @@ -80,3 +84,36 @@ def convert_input_schema(mci_schema: dict[str, Any]) -> dict[str, Any]:
return {"type": "object", "properties": mci_schema}

return mci_schema

@staticmethod
def convert_annotations(mci_annotations: Any) -> types.ToolAnnotations | None:
"""
Convert MCI Annotations to MCP ToolAnnotations format.

Transfers annotation fields from MCI tool annotations to the MCP format,
including title, readOnlyHint, destructiveHint, idempotentHint, and openWorldHint.

Args:
mci_annotations: Annotations object from MCI tool definition (or None)

Returns:
ToolAnnotations object compatible with MCP protocol, or None if no annotations

Example:
>>> from mcipy.models import Annotations
>>> mci_ann = Annotations(title="My Tool", readOnlyHint=True)
>>> mcp_ann = MCIToolConverter.convert_annotations(mci_ann)
>>> print(mcp_ann.title, mcp_ann.readOnlyHint)
"""
if mci_annotations is None:
return None

# Convert MCI Annotations to MCP ToolAnnotations
# Both models have the same field structure, so we can extract and transfer
return types.ToolAnnotations(
title=mci_annotations.title,
readOnlyHint=mci_annotations.readOnlyHint,
destructiveHint=mci_annotations.destructiveHint,
idempotentHint=mci_annotations.idempotentHint,
openWorldHint=mci_annotations.openWorldHint,
)
88 changes: 88 additions & 0 deletions tests/test_mcp_server_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,91 @@ async def test_multiple_servers_from_same_client():

finally:
Path(schema_path).unlink()


@pytest.mark.asyncio
async def test_server_preserves_annotations():
"""
Test that tool annotations are preserved when creating MCP server.

Verifies that annotations from MCI tools (title, readOnlyHint, destructiveHint,
idempotentHint, openWorldHint) are correctly transferred to MCP tools in the server.
"""
schema = {
"schemaVersion": "1.0",
"tools": [
{
"name": "delete_resource",
"description": "Delete a resource from the remote server",
"annotations": {
"title": "Delete Resource",
"readOnlyHint": False,
"destructiveHint": True,
"idempotentHint": False,
"openWorldHint": True,
},
"inputSchema": {
"type": "object",
"properties": {"id": {"type": "string"}},
"required": ["id"],
},
"execution": {"type": "text", "text": "Deleted resource {{props.id}}"},
},
{
"name": "read_data",
"description": "Read data from the server",
"annotations": {
"title": "Read Data",
"readOnlyHint": True,
},
"execution": {"type": "text", "text": "Reading data..."},
},
{
"name": "no_annotations",
"description": "Tool without annotations",
"execution": {"type": "text", "text": "No annotations"},
},
],
}

schema_path = create_test_schema(schema)
try:
# Load schema and create server
mci_client = MCIClient(schema_file_path=schema_path)
tools = mci_client.tools()

builder = MCPServerBuilder(mci_client)
server = await builder.create_server("annotated-server")

await builder.register_all_tools(server, tools)

mcp_tools = server._mci_tools # type: ignore

# Verify annotations for delete_resource
delete_tool = mcp_tools[0]
assert delete_tool.name == "delete_resource"
assert delete_tool.annotations is not None
assert delete_tool.annotations.title == "Delete Resource"
assert delete_tool.annotations.readOnlyHint is False
assert delete_tool.annotations.destructiveHint is True
assert delete_tool.annotations.idempotentHint is False
assert delete_tool.annotations.openWorldHint is True

# Verify annotations for read_data
read_tool = mcp_tools[1]
assert read_tool.name == "read_data"
assert read_tool.annotations is not None
assert read_tool.annotations.title == "Read Data"
assert read_tool.annotations.readOnlyHint is True
# Other fields should be None (not set in MCI)
assert read_tool.annotations.destructiveHint is None
assert read_tool.annotations.idempotentHint is None
assert read_tool.annotations.openWorldHint is None

# Verify no annotations for no_annotations tool
no_ann_tool = mcp_tools[2]
assert no_ann_tool.name == "no_annotations"
assert no_ann_tool.annotations is None

finally:
Path(schema_path).unlink()
Loading