casual-mcp 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
- from pydantic import BaseModel
1
+ from pydantic import BaseModel, Field
2
2
 
3
3
  from casual_mcp.models.mcp_server_config import McpServerConfig
4
4
  from casual_mcp.models.model_config import McpModelConfig
5
+ from casual_mcp.models.toolset_config import ToolSetConfig
5
6
 
6
7
 
7
8
  class Config(BaseModel):
8
9
  namespace_tools: bool | None = False
9
10
  models: dict[str, McpModelConfig]
10
11
  servers: dict[str, McpServerConfig]
12
+ tool_sets: dict[str, ToolSetConfig] = Field(default_factory=dict)
@@ -0,0 +1,40 @@
1
+ """Toolset configuration models for filtering available tools."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ExcludeSpec(BaseModel):
7
+ """Specification for excluding specific tools from a server."""
8
+
9
+ exclude: list[str] = Field(description="List of tool names to exclude")
10
+
11
+
12
+ # Tool specification: true (all), list (include specific), or exclude object
13
+ ToolSpec = bool | list[str] | ExcludeSpec
14
+
15
+
16
+ class ToolSetConfig(BaseModel):
17
+ """Configuration for a named toolset.
18
+
19
+ A toolset defines which tools from which servers should be available
20
+ during a chat session. Each server can be configured to:
21
+ - Include all tools (True)
22
+ - Include specific tools (list of tool names)
23
+ - Include all except specific tools (ExcludeSpec)
24
+
25
+ Example:
26
+ {
27
+ "description": "Research tools",
28
+ "servers": {
29
+ "wikimedia": True,
30
+ "search": ["brave_web_search"],
31
+ "fetch": {"exclude": ["fetch_dangerous"]}
32
+ }
33
+ }
34
+ """
35
+
36
+ description: str = Field(default="", description="Human-readable description")
37
+ servers: dict[str, ToolSpec] = Field(
38
+ default_factory=dict,
39
+ description="Mapping of server name to tool specification",
40
+ )
@@ -0,0 +1,171 @@
1
+ """Tool filtering logic for toolsets.
2
+
3
+ This module provides functionality to filter MCP tools based on toolset
4
+ configurations, including validation to ensure referenced servers and
5
+ tools actually exist.
6
+ """
7
+
8
+ import mcp
9
+
10
+ from casual_mcp.logging import get_logger
11
+ from casual_mcp.models.toolset_config import ExcludeSpec, ToolSetConfig
12
+
13
+ logger = get_logger("tool_filter")
14
+
15
+
16
+ class ToolSetValidationError(Exception):
17
+ """Raised when a toolset references invalid servers or tools."""
18
+
19
+ pass
20
+
21
+
22
+ def extract_server_and_tool(tool_name: str, server_names: set[str]) -> tuple[str, str]:
23
+ """Extract server name and base tool name from a potentially prefixed tool name.
24
+
25
+ When multiple servers are configured, fastmcp prefixes tools as "serverName_toolName".
26
+ When a single server is configured, tools are not prefixed.
27
+
28
+ Args:
29
+ tool_name: The full tool name (possibly prefixed)
30
+ server_names: Set of configured server names
31
+
32
+ Returns:
33
+ Tuple of (server_name, base_tool_name)
34
+ """
35
+ if "_" in tool_name:
36
+ prefix = tool_name.split("_", 1)[0]
37
+ if prefix in server_names:
38
+ return prefix, tool_name.split("_", 1)[1]
39
+
40
+ # Single server case - return the single server name
41
+ if len(server_names) == 1:
42
+ return next(iter(server_names)), tool_name
43
+
44
+ # Fallback - can't determine server
45
+ return "default", tool_name
46
+
47
+
48
+ def _build_server_tool_map(tools: list[mcp.Tool], server_names: set[str]) -> dict[str, set[str]]:
49
+ """Build a mapping of server names to their available tool names.
50
+
51
+ Args:
52
+ tools: List of MCP tools
53
+ server_names: Set of configured server names
54
+
55
+ Returns:
56
+ Dict mapping server name to set of base tool names
57
+ """
58
+ server_tool_map: dict[str, set[str]] = {name: set() for name in server_names}
59
+
60
+ for tool in tools:
61
+ server_name, base_name = extract_server_and_tool(tool.name, server_names)
62
+ if server_name in server_tool_map:
63
+ server_tool_map[server_name].add(base_name)
64
+
65
+ return server_tool_map
66
+
67
+
68
+ def validate_toolset(
69
+ toolset: ToolSetConfig,
70
+ tools: list[mcp.Tool],
71
+ server_names: set[str],
72
+ ) -> None:
73
+ """Validate that a toolset references only valid servers and tools.
74
+
75
+ Args:
76
+ toolset: The toolset configuration to validate
77
+ tools: List of available MCP tools
78
+ server_names: Set of configured server names
79
+
80
+ Raises:
81
+ ToolSetValidationError: If the toolset references non-existent servers or tools
82
+ """
83
+ server_tool_map = _build_server_tool_map(tools, server_names)
84
+ errors: list[str] = []
85
+
86
+ for server_name, tool_spec in toolset.servers.items():
87
+ # Check server exists
88
+ if server_name not in server_names:
89
+ errors.append(f"Server '{server_name}' not found in configuration")
90
+ continue
91
+
92
+ available = server_tool_map.get(server_name, set())
93
+
94
+ # Validate tool names in include list
95
+ if isinstance(tool_spec, list):
96
+ for tool_name in tool_spec:
97
+ if tool_name not in available:
98
+ errors.append(
99
+ f"Tool '{tool_name}' not found in server '{server_name}'. "
100
+ f"Available: {sorted(available)}"
101
+ )
102
+
103
+ # Validate tool names in exclude list
104
+ elif isinstance(tool_spec, ExcludeSpec):
105
+ for tool_name in tool_spec.exclude:
106
+ if tool_name not in available:
107
+ errors.append(
108
+ f"Tool '{tool_name}' not found in server '{server_name}' "
109
+ f"(specified in exclude list). Available: {sorted(available)}"
110
+ )
111
+
112
+ if errors:
113
+ raise ToolSetValidationError("\n".join(errors))
114
+
115
+
116
+ def filter_tools_by_toolset(
117
+ tools: list[mcp.Tool],
118
+ toolset: ToolSetConfig,
119
+ server_names: set[str],
120
+ validate: bool = True,
121
+ ) -> list[mcp.Tool]:
122
+ """Filter a list of MCP tools based on a toolset configuration.
123
+
124
+ Args:
125
+ tools: Full list of available MCP tools
126
+ toolset: The toolset configuration to apply
127
+ server_names: Set of configured server names
128
+ validate: Whether to validate the toolset first (raises on invalid)
129
+
130
+ Returns:
131
+ Filtered list of tools matching the toolset
132
+
133
+ Raises:
134
+ ToolSetValidationError: If validate=True and toolset is invalid
135
+ """
136
+ if validate:
137
+ validate_toolset(toolset, tools, server_names)
138
+
139
+ filtered: list[mcp.Tool] = []
140
+
141
+ for tool in tools:
142
+ server_name, base_name = extract_server_and_tool(tool.name, server_names)
143
+
144
+ # Check if this server is in the toolset
145
+ if server_name not in toolset.servers:
146
+ continue
147
+
148
+ tool_spec = toolset.servers[server_name]
149
+
150
+ # Determine if tool should be included
151
+ include = False
152
+
153
+ if tool_spec is True:
154
+ # All tools from this server
155
+ include = True
156
+ elif isinstance(tool_spec, list):
157
+ # Only specific tools
158
+ include = base_name in tool_spec
159
+ elif isinstance(tool_spec, ExcludeSpec):
160
+ # All except excluded tools
161
+ include = base_name not in tool_spec.exclude
162
+
163
+ if include:
164
+ filtered.append(tool)
165
+
166
+ logger.debug(
167
+ f"Filtered {len(tools)} tools to {len(filtered)} using toolset "
168
+ f"with {len(toolset.servers)} servers"
169
+ )
170
+
171
+ return filtered
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: casual-mcp
3
+ Version: 0.7.0
4
+ Summary: Multi-server MCP client for LLM tool orchestration
5
+ Author: Alex Stansfield
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/casualgenius/casual-mcp
8
+ Project-URL: Repository, https://github.com/casualgenius/casual-mcp
9
+ Project-URL: Issue Tracker, https://github.com/casualgenius/casual-mcp/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: casual-llm[openai]>=0.4.3
14
+ Requires-Dist: dateparser>=1.2.1
15
+ Requires-Dist: fastapi>=0.115.12
16
+ Requires-Dist: fastmcp>=2.12.4
17
+ Requires-Dist: jinja2>=3.1.6
18
+ Requires-Dist: python-dotenv>=1.1.0
19
+ Requires-Dist: questionary>=2.1.0
20
+ Requires-Dist: requests>=2.32.3
21
+ Requires-Dist: rich>=14.0.0
22
+ Requires-Dist: typer>=0.19.2
23
+ Requires-Dist: uvicorn>=0.34.2
24
+ Dynamic: license-file
25
+
26
+ # Casual MCP
27
+
28
+ ![PyPI](https://img.shields.io/pypi/v/casual-mcp)
29
+ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
30
+
31
+ **Casual MCP** is a Python framework for building, evaluating, and serving LLMs with tool-calling capabilities using [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
32
+
33
+ ## Features
34
+
35
+ - Multi-server MCP client using [FastMCP](https://github.com/jlowin/fastmcp)
36
+ - OpenAI and Ollama provider support (via [casual-llm](https://github.com/AlexStansfield/casual-llm))
37
+ - Recursive tool-calling chat loop
38
+ - Toolsets for selective tool filtering per request
39
+ - Usage statistics tracking (tokens, tool calls, LLM calls)
40
+ - System prompt templating with Jinja2
41
+ - CLI and API interfaces
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Using uv
47
+ uv add casual-mcp
48
+
49
+ # Using pip
50
+ pip install casual-mcp
51
+ ```
52
+
53
+ For development:
54
+
55
+ ```bash
56
+ git clone https://github.com/casualgenius/casual-mcp.git
57
+ cd casual-mcp
58
+ uv sync --group dev
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ 1. Create `casual_mcp_config.json`:
64
+
65
+ ```json
66
+ {
67
+ "models": {
68
+ "gpt-4.1": { "provider": "openai", "model": "gpt-4.1" }
69
+ },
70
+ "servers": {
71
+ "time": { "command": "python", "args": ["mcp-servers/time/server.py"] }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 2. Set your API key: `export OPENAI_API_KEY=your-key`
77
+
78
+ 3. Start the server: `casual-mcp serve`
79
+
80
+ 4. Make a request:
81
+
82
+ ```bash
83
+ curl -X POST http://localhost:8000/generate \
84
+ -H "Content-Type: application/json" \
85
+ -d '{"model": "gpt-4.1", "prompt": "What time is it?"}'
86
+ ```
87
+
88
+ ## Configuration
89
+
90
+ Configure models, MCP servers, and toolsets in `casual_mcp_config.json`.
91
+
92
+ ```json
93
+ {
94
+ "models": {
95
+ "gpt-4.1": { "provider": "openai", "model": "gpt-4.1" }
96
+ },
97
+ "servers": {
98
+ "time": { "command": "python", "args": ["server.py"] },
99
+ "weather": { "url": "http://localhost:5050/mcp" }
100
+ },
101
+ "tool_sets": {
102
+ "basic": { "description": "Basic tools", "servers": { "time": true } }
103
+ }
104
+ }
105
+ ```
106
+
107
+ See [Configuration Guide](docs/configuration.md) for full details on models, servers, toolsets, and templates.
108
+
109
+ ## CLI
110
+
111
+ ```bash
112
+ casual-mcp serve # Start API server
113
+ casual-mcp servers # List configured servers
114
+ casual-mcp models # List configured models
115
+ casual-mcp toolsets # Manage toolsets interactively
116
+ casual-mcp tools # List available tools
117
+ ```
118
+
119
+ See [CLI & API Reference](docs/cli-api.md) for all commands and options.
120
+
121
+ ## API
122
+
123
+ | Endpoint | Description |
124
+ |----------|-------------|
125
+ | `POST /chat` | Send message history |
126
+ | `POST /generate` | Send prompt with optional session |
127
+ | `GET /generate/session/{id}` | Get session messages |
128
+ | `GET /toolsets` | List available toolsets |
129
+
130
+ See [CLI & API Reference](docs/cli-api.md#api-endpoints) for request/response formats.
131
+
132
+ ## Programmatic Usage
133
+
134
+ ```python
135
+ from casual_llm import SystemMessage, UserMessage
136
+ from casual_mcp import McpToolChat, ProviderFactory, load_config, load_mcp_client
137
+
138
+ config = load_config("casual_mcp_config.json")
139
+ mcp_client = load_mcp_client(config)
140
+
141
+ provider_factory = ProviderFactory()
142
+ provider = provider_factory.get_provider("gpt-4.1", config.models["gpt-4.1"])
143
+
144
+ chat = McpToolChat(mcp_client, provider)
145
+ messages = [
146
+ SystemMessage(content="You are a helpful assistant."),
147
+ UserMessage(content="What time is it?")
148
+ ]
149
+ response = await chat.chat(messages)
150
+ ```
151
+
152
+ See [Programmatic Usage Guide](docs/programmatic-usage.md) for `McpToolChat`, usage statistics, toolsets, and common patterns.
153
+
154
+ ## Architecture
155
+
156
+ Casual MCP orchestrates LLMs and MCP tool servers in a recursive loop:
157
+
158
+ ```
159
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
160
+ │ MCP Servers │─────▶│ Tool Cache │─────▶│ Tool Converter│
161
+ └─────────────┘ └──────────────┘ └─────────────┘
162
+ │ │
163
+ ▼ ▼
164
+ ┌──────────────────────────────┐
165
+ │ McpToolChat Loop │
166
+ │ │
167
+ │ LLM ──▶ Tool Calls ──▶ MCP │
168
+ │ ▲ │ │
169
+ │ └──────── Results ─────┘ │
170
+ └──────────────────────────────┘
171
+ ```
172
+
173
+ 1. **MCP Client** connects to tool servers (local stdio or remote HTTP/SSE)
174
+ 2. **Tool Cache** fetches and caches tools from all servers
175
+ 3. **ProviderFactory** creates LLM providers from casual-llm
176
+ 4. **McpToolChat** runs the recursive loop until the LLM provides a final answer
177
+
178
+ ## Environment Variables
179
+
180
+ | Variable | Default | Description |
181
+ |----------|---------|-------------|
182
+ | `OPENAI_API_KEY` | - | Required for OpenAI provider |
183
+ | `TOOL_RESULT_FORMAT` | `result` | `result`, `function_result`, or `function_args_result` |
184
+ | `MCP_TOOL_CACHE_TTL` | `30` | Tool cache TTL in seconds (0 = indefinite) |
185
+ | `LOG_LEVEL` | `INFO` | Logging level |
186
+
187
+ ## Troubleshooting
188
+
189
+ Common issues and solutions are covered in the [Troubleshooting Guide](docs/troubleshooting.md).
190
+
191
+ ## License
192
+
193
+ [MIT License](LICENSE)
@@ -1,21 +1,23 @@
1
1
  casual_mcp/__init__.py,sha256=eeI1TIj8Cu-H4OMV64LaNqVqo4wSFaGu7215hJeN_HM,598
2
- casual_mcp/cli.py,sha256=2-0sTxfNfQSukBtg0Xs9P6VrAMZ89SqJ9VJzOM68d-o,2129
2
+ casual_mcp/cli.py,sha256=yzNkS2rZ8Lb-1nGFY75RUaixHf1x-vf6gdUPU58jOlg,14953
3
3
  casual_mcp/convert_tools.py,sha256=mlH18DTGGeWb0Vxfj1cUSMhTGRE9z8q_xWrVXvpg3mE,1742
4
4
  casual_mcp/logging.py,sha256=S2XpLIKHHDtmru_YBFLdMamdmYRm16Yw3tshE3g3Wqg,932
5
- casual_mcp/main.py,sha256=aI3isW0Wzny_iubx8HlNgBVvYEeBe-Jrrdbp80oYmk4,4299
6
- casual_mcp/mcp_tool_chat.py,sha256=Evc5LMfUYicl7jlix42QURYaq0cI2CIUg0q-344cjUg,8401
5
+ casual_mcp/main.py,sha256=OxW6itdauq23QyxPhCt6K0kM2jV2WlP_JSfk5IKSN48,6057
6
+ casual_mcp/mcp_tool_chat.py,sha256=h9-1wzgLIhm6kfaNHNJ3RGwkL3A3Q3EoUZvnxIroL74,11144
7
7
  casual_mcp/provider_factory.py,sha256=Jp2HQOJdlDDed-hfZf1drEVbw0kpZSE0TN9G0Dcp4w8,1260
8
8
  casual_mcp/tool_cache.py,sha256=VE599sF7vHH6megcueqVxCZavvTcoFDoZu2QuZM3cYA,3161
9
+ casual_mcp/tool_filter.py,sha256=u1C8w0CNZ4AH4bbkIRz0ph9-g9-OxSBnkZiLJVapyHo,5430
9
10
  casual_mcp/utils.py,sha256=XxzPxQ3j97edeCRXtoO8lJS9R0JYOa25p2MJNwGapJA,3201
10
- casual_mcp/models/__init__.py,sha256=byhteS6fueIdtoaQYL2w5hcBJmJhXF7X7YhGslvscco,786
11
+ casual_mcp/models/__init__.py,sha256=2D3grALibdsymdpI82EHVGCgEKCszU418qh61os7_rM,924
11
12
  casual_mcp/models/chat_stats.py,sha256=ZjeZ_ckx-SfioYs39NAaQxK6qPG9SlFlrB7j7jHZ40w,1221
12
- casual_mcp/models/config.py,sha256=LcqtfW3w7iqrT3FnW50L1mgqAvD_OsYk4ySBZZVV-GI,300
13
+ casual_mcp/models/config.py,sha256=JsgRtZzA3jZALfAKj-nMIJjnd4RmGmJsLUdzQ_6wCZk,436
13
14
  casual_mcp/models/generation_error.py,sha256=abDAahS2fhYkS-ARng1Tk7oudoAO4imkoKYcC9PHT2U,272
14
15
  casual_mcp/models/mcp_server_config.py,sha256=0OHsHUEKxRoCl21lsye4E5GoCNmdZWIZCOOthcTpdsE,539
15
16
  casual_mcp/models/model_config.py,sha256=59Y7MvcboPKdAilSwUyeC7lfRm4aYkFhZ5c8EVRP5ys,425
16
- casual_mcp-0.6.0.dist-info/licenses/LICENSE,sha256=U3Zu2tkrh5vXdy7gIdE8WJGM9D4gGp3hohAAWdre-yo,1058
17
- casual_mcp-0.6.0.dist-info/METADATA,sha256=GQLuEXfducugyuUHjB3qklz8FAOZ7go3PQ0d7Pqb2ZI,22218
18
- casual_mcp-0.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
19
- casual_mcp-0.6.0.dist-info/entry_points.txt,sha256=X48Np2cwl-SlRQdV26y2vPZ-2tJaODgZeVtfpHho-zg,50
20
- casual_mcp-0.6.0.dist-info/top_level.txt,sha256=K4CiI0Jf8PHICjuQVm32HuNMB44kp8Lb02bbbdiH5bo,11
21
- casual_mcp-0.6.0.dist-info/RECORD,,
17
+ casual_mcp/models/toolset_config.py,sha256=k95TqhHXYDonAt0GG5akzZRWzQ6rZG77eLB7QlSV5AA,1246
18
+ casual_mcp-0.7.0.dist-info/licenses/LICENSE,sha256=U3Zu2tkrh5vXdy7gIdE8WJGM9D4gGp3hohAAWdre-yo,1058
19
+ casual_mcp-0.7.0.dist-info/METADATA,sha256=8S2ssCaPlmVS3zYobzuzRKsLSyyKP_QHzjRCAtfdP80,6211
20
+ casual_mcp-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ casual_mcp-0.7.0.dist-info/entry_points.txt,sha256=X48Np2cwl-SlRQdV26y2vPZ-2tJaODgZeVtfpHho-zg,50
22
+ casual_mcp-0.7.0.dist-info/top_level.txt,sha256=K4CiI0Jf8PHICjuQVm32HuNMB44kp8Lb02bbbdiH5bo,11
23
+ casual_mcp-0.7.0.dist-info/RECORD,,