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
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- run: pip install -e ".[dev]"

- run: pytest tests/ -v
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
tmp/*
__pycache__/

.pytest_cache/
*.egg-info/
99 changes: 97 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,11 @@ custom_function = {
},
}

# MCP tools
# MCP tools (connect to any MCP server)
mcp_tool = {
"type": "mcp",
"url": "https://mcp.example.com",
"allow": ["read", "write"],
"allowedTools": ["search", "get_page"],
}
```

Expand Down Expand Up @@ -275,6 +275,101 @@ tool_with_headers_and_defaults = {

Each tool can have its own headers and defaults - they're only applied when that specific tool is called.

### MCP Tools

Connect to any [Model Context Protocol](https://modelcontextprotocol.io/) server and use its tools in your runs. Subconscious discovers tools from the server, filters by your `allowedTools` list, and proxies calls automatically.

#### Authentication

MCP servers that require authentication accept an `auth` object. The auth translates to an HTTP header sent with every tool call:

| Method | When to use | Header sent |
| --- | --- | --- |
| **Bearer** | Most common — OAuth tokens, JWTs, etc. | `Authorization: Bearer <token>` |
| **API key** | Service-specific API keys | `<header>: <token>` (header is typically `X-Api-Key` — check your MCP server's docs) |

```python
from subconscious import Subconscious, MCPTool, McpAuth

client = Subconscious()

# Basic — use all tools from an MCP server
run = client.run(
engine="tim-gpt",
input={
"instructions": "Find my recent meeting notes",
"tools": [
MCPTool(url="https://mcp.notion.so/v1"),
],
},
options={"await_completion": True},
)

# Filter to specific tools
run = client.run(
engine="tim-gpt",
input={
"instructions": "Search my documents",
"tools": [
MCPTool(
url="https://mcp.notion.so/v1",
allowed_tools=["search", "get_page"], # case-insensitive
),
],
},
options={"await_completion": True},
)

# With bearer auth (most common — e.g. OAuth tokens)
# → sends header: { "Authorization": "Bearer <token>" }
run = client.run(
engine="tim-gpt",
input={
"instructions": "Check my calendar",
"tools": [
MCPTool(
url="https://mcp.google.com/v1",
auth=McpAuth(type="bearer", token="your-oauth-token"),
),
],
},
options={"await_completion": True},
)

# API key auth with custom header
# → sends header: { "X-Api-Key": "<token>" }
# The header name is typically "X-Api-Key" but may vary —
# check the docs of the MCP server you are connecting to.
run = client.run(
engine="tim-gpt",
input={
"instructions": "Query the database",
"tools": [
MCPTool(
url="https://mcp.example.com",
auth=McpAuth(type="api_key", token="key123", header="X-Api-Key"),
),
],
},
options={"await_completion": True},
)
```

**`allowedTools` filtering:**

| Value | Behavior |
| --- | --- |
| Omitted / `None` | All tools from the server are enabled |
| `["*"]` | All tools enabled (explicit wildcard) |
| `["search", "fetch"]` | Only these tools (case-insensitive) |
| `[]` | No tools (blocks all) |

You can also pass MCP tools as plain dicts:

```python
{"type": "mcp", "url": "https://mcp.example.com", "allowedTools": ["search"]}
```

### Error Handling

```python
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "subconscious-sdk"
version = "0.2.1"
version = "0.3.0"
description = "The official Python SDK for the Subconscious API"
readme = "README.md"
authors = [
Expand Down
4 changes: 3 additions & 1 deletion subconscious/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
PlatformTool,
FunctionTool,
MCPTool,
McpAuth,
# Stream events
StreamEvent,
DeltaEvent,
Expand All @@ -41,7 +42,7 @@
ValidationError,
)

__version__ = "0.2.1"
__version__ = "0.3.0"
__author__ = "Subconscious Systems"
__email__ = "contact@subconscious.dev"

Expand All @@ -66,6 +67,7 @@
"PlatformTool",
"FunctionTool",
"MCPTool",
"McpAuth",
# Stream events
"StreamEvent",
"DeltaEvent",
Expand Down
54 changes: 33 additions & 21 deletions subconscious/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,36 @@ def _resolve_schema(schema: Any) -> Optional[Dict[str, Any]]:
# Unknown type - try to use it as-is
return schema

# Python snake_case → API camelCase key mapping for tool serialization
_TOOL_KEY_MAP = {
"allowed_tools": "allowedTools",
}


def _normalize_tool(tool: Any) -> Dict[str, Any]:
"""Convert a tool dataclass to an API-compatible dict.

Strips None values and maps snake_case keys to camelCase.
"""
if not hasattr(tool, "__dict__"):
return tool

result = {}
for k, v in tool.__dict__.items():
if v is None:
continue
# Recursively normalize nested dataclasses (e.g. McpAuth)
if hasattr(v, "__dict__"):
v = {
_TOOL_KEY_MAP.get(nk, nk): nv
for nk, nv in v.__dict__.items()
if nv is not None
}
key = _TOOL_KEY_MAP.get(k, k)
result[key] = v
return result


TERMINAL_STATUSES: List[RunStatus] = ["succeeded", "failed", "canceled", "timed_out"]


Expand Down Expand Up @@ -205,16 +235,7 @@ class Result(BaseModel):
input_dict["reasoningFormat"] = _resolve_schema(input_dict["reasoningFormat"])

# Normalize tools to dicts
tools = input_dict.get("tools", [])
normalized_tools = []
for tool in tools:
if hasattr(tool, "__dict__"):
normalized_tools.append(
{k: v for k, v in tool.__dict__.items() if v is not None}
)
else:
normalized_tools.append(tool)
input_dict["tools"] = normalized_tools
input_dict["tools"] = [_normalize_tool(t) for t in input_dict.get("tools", [])]

# Make request
data = self._request(
Expand Down Expand Up @@ -375,17 +396,8 @@ class Result(BaseModel):
if "reasoningFormat" in input_dict:
input_dict["reasoningFormat"] = _resolve_schema(input_dict["reasoningFormat"])

# Normalize tools
tools = input_dict.get("tools", [])
normalized_tools = []
for tool in tools:
if hasattr(tool, "__dict__"):
normalized_tools.append(
{k: v for k, v in tool.__dict__.items() if v is not None}
)
else:
normalized_tools.append(tool)
input_dict["tools"] = normalized_tools
# Normalize tools to dicts
input_dict["tools"] = [_normalize_tool(t) for t in input_dict.get("tools", [])]

url = f"{self._base_url}/runs/stream"
headers = {
Expand Down
38 changes: 36 additions & 2 deletions subconscious/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,47 @@ class FunctionTool:
"""Parameter values hidden from model and injected at call time."""


@dataclass
class McpAuth:
"""MCP Authentication.

Used for MCP tools that require authentication.
Translates to an HTTP header sent with every tool call:

- Bearer: ``{ "Authorization": "Bearer <token>" }``
- API key: ``{ "<header>": "<token>" }``

Bearer auth is the most common method (e.g. OAuth tokens).
For API key auth, the header is typically ``X-Api-Key`` but may vary —
check the documentation of the MCP server you are connecting to.

Attributes:
type: Auth method — ``"bearer"`` or ``"api_key"``.
token: The token or key value.
header: For ``api_key`` auth only, the header name to send the token
in (e.g. ``"X-Api-Key"``).
"""

type: Literal["bearer", "api_key"]
token: str
header: Optional[str] = None


@dataclass
class MCPTool:
"""An MCP (Model Context Protocol) tool."""
"""An MCP (Model Context Protocol) tool.

Attributes:
url: URL of the MCP server
allowed_tools: Tool names to enable. Case-insensitive.
["*"] or omit for all tools. [] blocks all.
auth: Optional authentication for the MCP server
"""

url: str
type: Literal["mcp"] = "mcp"
allow: Optional[List[str]] = None
allowed_tools: Optional[List[str]] = None
auth: Optional[McpAuth] = None


# Tool union type
Expand Down
Empty file added tests/__init__.py
Empty file.
Loading
Loading