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
5 changes: 5 additions & 0 deletions pctx-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,11 @@ p = Pctx(tools=[custom_function], servers=servers)
pip install pctx-client[langchain]
```

- `claude_agent_sdk`: Export pctx's CodeMode tools as [Claude Agent SDK tools](https://platform.claude.com/docs/en/agent-sdk/python#tool)
```bash
pip install pctx-client[claude]
```

- `crewai`: Export pctx's Code Mode tools as [CrewAI tools](https://docs.crewai.com/en/concepts/tools)

```bash
Expand Down
2 changes: 2 additions & 0 deletions pctx-py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ crewai = ["crewai>=1.6.1"]
openai = ["openai-agents>=0.12.0"]
pydantic-ai = ["pydantic-ai>=1.60.0"]
bm25s = ["bm25s[stem]>=0.2.12"]
claude = ["claude-agent-sdk>=0.1.55"]

[project.urls]
"Homepage" = "https://github.com/portofcontext/pctx"
Expand All @@ -45,6 +46,7 @@ dev = [
"crewai>=1.6.1",
"openai-agents>=0.12.0",
"pydantic-ai>=1.60.0",
"claude-agent-sdk>=0.1.55",
"bm25s[stem]>=0.2.12",
"sphinx",
"sphinx-autobuild",
Expand Down
114 changes: 113 additions & 1 deletion pctx-py/src/pctx_client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import asyncio
import warnings
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

from httpx import AsyncClient
Expand Down Expand Up @@ -37,6 +37,7 @@
try:
from agents import FunctionTool
from bm25s import BM25
from claude_agent_sdk import SdkMcpTool as ClaudeSdkMcpTool
from crewai.tools import BaseTool as CrewAiBaseTool
from langchain_core.tools import BaseTool as LangchainBaseTool
from pydantic_ai.tools import Tool as PydanticAITool
Expand Down Expand Up @@ -826,3 +827,114 @@ async def get_function_details(functions: list[str]) -> str:

# filter according to disclosure
return [t for t in all_tools if disclosure.contains_tool(t.name)]

def claude_agent_sdk_tools(
self,
disclosure: ToolDisclosure | ToolDisclosureName = ToolDisclosure.CATALOG,
descriptions: dict[ToolName, str] | None = None,
) -> "list[ClaudeSdkMcpTool]":
"""
Expose PCTX tools as Claude Agent SDK tools

Args:
disclosure: Controls which tools are exposed and how function context is
provided to the model. CATALOG (default) exposes list_functions,
get_function_details, and execute_typescript — the agent discovers
and retrieves function signatures before executing. FS exposes
execute_bash and execute_typescript — the agent browses the virtual
filesystem directly.
descriptions: Optional custom descriptions to override defaults.

Requires the 'claude' extra to be installed:
pip install pctx[claude]

Raises:
ImportError: If claude is not installed.

Examples:
>>> tools = pctx.claude_agent_sdk_tools() # default: catalog
>>> tools = pctx.claude_agent_sdk_tools(disclosure="filesystem")
>>> tools = pctx.claude_agent_sdk_tools(descriptions={"execute_typescript": "Custom"})
"""
disclosure = ToolDisclosure(disclosure)
try:
from claude_agent_sdk import tool as claude_tool
except ImportError as e:
raise ImportError(
"Claude Agent SDK is not installed. Install it with: pip install pctx[claude]"
) from e

def _text_content_block(val: str) -> dict:
return {"content": [{"type": "text", "text": val}]}

# build all tools
@claude_tool(
"execute_bash",
get_tool_description("execute_bash", overrides=descriptions),
ExecuteBashInput.model_json_schema(),
)
async def execute_bash(args: dict[str, Any]) -> str:
tool_input = ExecuteBashInput(**args)
bash_out = await self.execute_bash(tool_input.command)
return _text_content_block(bash_out.markdown())

@claude_tool(
"execute_typescript",
get_tool_description(
"execute_typescript", disclosure=disclosure, overrides=descriptions
),
ExecuteTypescriptInput.model_json_schema(),
)
async def execute_typescript(args: dict[str, Any]) -> str:
tool_input = ExecuteTypescriptInput(**args)
exec_out = await self.execute_typescript(
tool_input.code, disclosure=disclosure
)
return _text_content_block(exec_out.markdown())

@claude_tool(
"list_functions",
get_tool_description("list_functions", overrides=descriptions),
{"type": "object"},
)
async def list_functions(_args: dict[str, Any]) -> str:
listed = await self.list_functions()
return _text_content_block(listed.code)

@claude_tool(
"get_function_details",
get_tool_description("get_function_details", overrides=descriptions),
GetFunctionDetailsInput.model_json_schema(),
)
async def get_function_details(args: dict[str, Any]) -> str:
tool_input = GetFunctionDetailsInput(**args)
details = await self.get_function_details(tool_input.functions)
return _text_content_block(details.code)

class SearchFunctionsInput(BaseModel):
query: str
k: int = 10

@claude_tool(
"search_functions",
get_tool_description("search_functions", overrides=descriptions),
SearchFunctionsInput.model_json_schema(),
)
async def search_functions(args: dict[str, Any]) -> str:
print(f"Claude fn called search_functions: {args}")
tool_input = SearchFunctionsInput(**args)
functions = await self.search_functions(tool_input.query, tool_input.k)
return _text_content_block(
self._search_functions_result_to_string(functions)
)

all_tools = [
execute_bash,
execute_typescript,
list_functions,
search_functions,
get_function_details,
]

# filter according to disclosure
return [t for t in all_tools if disclosure.contains_tool(t.name)]
37 changes: 37 additions & 0 deletions pctx-py/tests/scripts/claude_agent_sdk_code_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio

from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, query
from rich import print

from pctx_client import Pctx, tool


@tool
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"


@tool
def get_time(city: str) -> str:
"""Get time for a given city."""
return f"It is midnight in {city}!"


async def run_agent():
async with Pctx(tools=[get_weather, get_time]) as p:
claude_tools = p.claude_agent_sdk_tools()
mcp = create_sdk_mcp_server(name="weather + time codemode", tools=claude_tools)
print([f"mcp__tools__{t.name}" for t in claude_tools])
async for message in query(
prompt="You are a helpful assistant, use tools when you need to access real-time information, you must use the tools mcp not websearch. What is the weather & time in SF?",
options=ClaudeAgentOptions(
mcp_servers={"tools": mcp},
allowed_tools=[f"mcp__tools__{t.name}" for t in claude_tools],
),
):
print(message)


if __name__ == "__main__":
asyncio.run(run_agent())
29 changes: 27 additions & 2 deletions pctx-py/uv.lock

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

Loading