Skip to content

Commit b5f380e

Browse files
committed
fixed unit tests
1 parent 92487db commit b5f380e

File tree

4 files changed

+266
-28
lines changed

4 files changed

+266
-28
lines changed

docs/architecture/tool.md

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44

55
TalkPipe integrates with the Model Context Protocol (MCP) to enable LLMs to call custom tools during conversations. Tools are defined using the `TOOL` syntax in ChatterLang scripts, which compiles TalkPipe pipelines into callable functions that LLMs can invoke.
66

7+
## Unified Server Model
8+
9+
Both `TOOL` and `MCP_SERVER` use the same server name concept. The `mcp_server` parameter in `TOOL` references a server that can be either:
10+
- **Local server**: Automatically created when first `TOOL` uses it
11+
- **External server**: Explicitly defined via `MCP_SERVER`
12+
713
## TOOL Syntax
814

9-
The `TOOL` keyword in ChatterLang allows you to register TalkPipe pipelines as tools that can be used by LLMs. Tools are registered during script compilation and stored in FastMCP server instances.
15+
The `TOOL` keyword registers TalkPipe pipelines as tools that can be used by LLMs.
1016

1117
### Basic Syntax
1218

@@ -21,25 +27,99 @@ TOOL tool_name = "pipeline_string" [parameters];
2127
- **`description`** (optional): A description of what the tool does, shown to the LLM
2228
- **`input_param`** (optional): Input parameter specification in format `"name:type:description"` (e.g., `"item:int:Number to double"`)
2329
- **`param_name`**, **`param_type`**, **`param_desc`** (optional): Individual parameter specifications as an alternative to `input_param`
24-
- **`mcp_server`** (optional): The name of the MCP server to register the tool with. Defaults to `"mcp"` if not specified
30+
- **`mcp_server`** (optional): The name of the MCP server to register the tool with. References a server name (local or external). Defaults to `"mcp"` if not specified
31+
32+
## MCP_SERVER Syntax
2533

26-
### Example
34+
The `MCP_SERVER` keyword connects to external MCP servers. Once connected, tools from that server are available for use with `llmPrompt`.
35+
36+
### Basic Syntax
2737

2838
```chatterlang
29-
TOOL double = "| lambda[expression='item*2']" [input_param="item:int:Number to double", description="Doubles a number", mcp_server="math_tools"];
30-
TOOL add_ten = "| lambda[expression='item+10']" [input_param="item:int:Number to add 10 to", description="Adds 10 to a number", mcp_server="math_tools"];
39+
MCP_SERVER server_name = "url_or_config" [parameters];
3140
```
3241

33-
## Multiple MCP Servers
42+
### Parameters
43+
44+
- **`url`** (required): The URL or connection string for the external MCP server
45+
- **`transport`** (optional): Transport type - `"http"`, `"sse"`, or `"stdio"`. Defaults to `"http"`
46+
- **`auth`** (optional): Authentication type - `"bearer"` or `"oauth"`
47+
- **`token`** (optional): Bearer token for authentication
48+
- **`headers`** (optional): Custom HTTP headers as a dictionary
49+
- **`command`** (optional): For stdio transport - command to execute
50+
- **`args`** (optional): For stdio transport - command arguments
51+
- **`cwd`** (optional): For stdio transport - working directory
52+
- **`env`** (optional): For stdio transport - environment variables
53+
54+
The server is stored in `runtime.const_store[server_name]` and can be referenced in `llmPrompt` using `tools=server_name`.
55+
56+
## MCP Server Management
3457

35-
You can define multiple MCP servers in a single script by specifying different `mcp_server` values. Each server maintains its own set of tools.
58+
Both local and external MCP servers use the same server name system. The `mcp_server` parameter in `TOOL` references the server name defined by `MCP_SERVER` or created automatically.
59+
60+
### Local Servers (Created Automatically)
61+
62+
When you define a `TOOL` with a `mcp_server` parameter, a local FastMCP server is automatically created if it doesn't exist:
3663

3764
```chatterlang
3865
TOOL double = "| lambda[expression='item*2']" [input_param="item:int:Number to double", mcp_server="math_tools"];
39-
TOOL uppercase = "| lambda[expression='item.upper()']" [input_param="item:str:Text to uppercase", mcp_server="text_tools"];
66+
TOOL add_ten = "| lambda[expression='item+10']" [input_param="item:int:Number to add 10 to", mcp_server="math_tools"];
4067
```
4168

42-
Each MCP server instance is stored in `runtime.const_store[server_name]` and can be referenced directly in the script.
69+
The server `math_tools` is created automatically when the first tool is registered.
70+
71+
### External Servers (MCP_SERVER Syntax)
72+
73+
To connect to an external MCP server, use the `MCP_SERVER` keyword:
74+
75+
```chatterlang
76+
# Connect to an external MCP server over HTTP
77+
MCP_SERVER external_tools = "https://api.example.com/mcp" [transport="http", auth="bearer", token="your-token"];
78+
79+
# Tools from the external server are automatically available
80+
INPUT FROM echo[data="Use the external tools"]
81+
| llmPrompt[model="llama3.1", source="ollama", tools=external_tools]
82+
| print
83+
```
84+
85+
### MCP_SERVER Parameters
86+
87+
- **`url`** (required): The URL or connection string for the external server
88+
- **`transport`** (optional): Transport type - `"http"`, `"sse"`, or `"stdio"`. Defaults to `"http"`
89+
- **`auth`** (optional): Authentication type - `"bearer"` or `"oauth"`
90+
- **`token`** (optional): Bearer token for authentication
91+
- **`headers`** (optional): Custom HTTP headers as a dictionary
92+
- **`command`** (optional): For stdio transport - command to execute
93+
- **`args`** (optional): For stdio transport - command arguments
94+
- **`cwd`** (optional): For stdio transport - working directory
95+
- **`env`** (optional): For stdio transport - environment variables
96+
97+
### Mixed Usage
98+
99+
You can combine local tools and external servers in the same script:
100+
101+
```chatterlang
102+
# Connect to external server
103+
MCP_SERVER external_api = "https://api.example.com/mcp" [transport="http", auth="bearer", token="token"];
104+
105+
# Define local tools on a local server
106+
TOOL double = "| lambda[expression='item*2']" [input_param="item:int:Number to double", mcp_server="math_tools"];
107+
108+
# Use either server with llmPrompt
109+
INPUT FROM echo[data="Use math tools"]
110+
| llmPrompt[model="llama3.1", source="ollama", tools=math_tools]
111+
| print
112+
113+
INPUT FROM echo[data="Use external API"]
114+
| llmPrompt[model="llama3.1", source="ollama", tools=external_api]
115+
| print
116+
```
117+
118+
**Note**: You cannot register local `TOOL` definitions on an external `MCP_SERVER`. External servers provide their own tools that are already available. If you try to register a `TOOL` with `mcp_server` pointing to an external server, a warning will be logged and the tool registration will be skipped.
119+
120+
### Server Storage
121+
122+
All MCP servers (both local and external) are stored in `runtime.const_store[server_name]` and can be referenced directly in the script using the server name identifier.
43123

44124
## Using Tools with LLMPrompt
45125

@@ -60,11 +140,12 @@ The `tools` parameter accepts the MCP server identifier, which is automatically
60140

61141
### Compilation Process
62142

63-
1. **Parsing**: The `TOOL` definitions are parsed during script compilation, similar to `CONST` definitions
64-
2. **Grouping**: Tools are grouped by their `mcp_server` parameter
65-
3. **Server Creation**: FastMCP instances are created for each unique server name
66-
4. **Tool Registration**: Each tool is registered with its corresponding MCP server using `register_talkpipe_tool()`
67-
5. **Storage**: MCP server instances are stored in `runtime.const_store[server_name]` for later reference
143+
1. **Parsing**: The `MCP_SERVER` and `TOOL` definitions are parsed during script compilation, similar to `CONST` definitions
144+
2. **External Server Connection**: `MCP_SERVER` definitions create FastMCP Client connections to external servers
145+
3. **Grouping**: Tools are grouped by their `mcp_server` parameter
146+
4. **Local Server Creation**: For tools referencing servers that don't exist, local FastMCP server instances are created
147+
5. **Tool Registration**: Each tool is registered with its corresponding MCP server using `register_talkpipe_tool()` (only for local servers)
148+
6. **Storage**: All MCP server instances (local and external) are stored in `runtime.const_store[server_name]` for later reference
68149

69150
### Tool Execution
70151

src/talkpipe/chatterlang/compiler.py

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,88 @@
1919
logger = logging.getLogger(__name__)
2020

2121

22+
def _register_mcp_servers(mcp_servers: Dict[str, Dict[str, Any]], runtime: RuntimeComponent) -> None:
23+
"""Register external MCP server connections defined in the script.
24+
25+
Args:
26+
mcp_servers: Dictionary mapping server names to server definitions
27+
runtime: RuntimeComponent containing the MCP server instances
28+
"""
29+
try:
30+
from fastmcp import Client
31+
except ImportError:
32+
logger.warning("FastMCP is not installed. MCP servers will not be registered. Install with: pip install fastmcp")
33+
return
34+
35+
for server_name, server_def in mcp_servers.items():
36+
url = server_def.get('url')
37+
transport = server_def.get('transport', 'http')
38+
39+
try:
40+
if transport == 'http' or transport == 'sse':
41+
# HTTP/SSE transport - Client can be initialized with URL directly
42+
# For auth, we may need to use a config dict or set headers
43+
if server_def.get('auth') == 'bearer' and server_def.get('token'):
44+
# Use config dict for authenticated connections
45+
config = {
46+
"mcpServers": {
47+
server_name: {
48+
"transport": transport,
49+
"url": url,
50+
"auth": "bearer",
51+
"token": server_def.get('token')
52+
}
53+
}
54+
}
55+
client = Client(config)
56+
elif server_def.get('headers'):
57+
# Custom headers - would need to be passed via config
58+
config = {
59+
"mcpServers": {
60+
server_name: {
61+
"transport": transport,
62+
"url": url,
63+
"headers": server_def.get('headers')
64+
}
65+
}
66+
}
67+
client = Client(config)
68+
else:
69+
# Simple URL connection
70+
client = Client(url)
71+
elif transport == 'stdio':
72+
# Stdio transport - requires command
73+
command = server_def.get('command')
74+
if not command:
75+
logger.error(f"MCP_SERVER '{server_name}': 'command' is required for stdio transport")
76+
continue
77+
78+
config = {
79+
"mcpServers": {
80+
server_name: {
81+
"transport": "stdio",
82+
"command": command,
83+
}
84+
}
85+
}
86+
if server_def.get('args'):
87+
config["mcpServers"][server_name]["args"] = server_def.get('args')
88+
if server_def.get('cwd'):
89+
config["mcpServers"][server_name]["cwd"] = server_def.get('cwd')
90+
if server_def.get('env'):
91+
config["mcpServers"][server_name]["env"] = server_def.get('env')
92+
93+
client = Client(config)
94+
else:
95+
logger.error(f"MCP_SERVER '{server_name}': Unknown transport '{transport}'")
96+
continue
97+
98+
runtime.const_store[server_name] = client
99+
logger.debug(f"Created FastMCP Client for external server '{server_name}' (transport: {transport})")
100+
except Exception as e:
101+
logger.error(f"Failed to create MCP_SERVER '{server_name}': {e}")
102+
103+
22104
def _register_tools(tools: Dict[str, Dict[str, Any]], runtime: RuntimeComponent) -> None:
23105
"""Register tools defined in the script with FastMCP.
24106
@@ -49,13 +131,21 @@ def _register_tools(tools: Dict[str, Dict[str, Any]], runtime: RuntimeComponent)
49131

50132
# Create or get MCP instances for each server and register tools
51133
for server_name, tool_list in tools_by_server.items():
52-
# Get or create FastMCP instance for this server
134+
# Get existing MCP instance (could be FastMCP server or Client)
53135
mcp = runtime.const_store.get(server_name)
54136
if mcp is None:
55-
# Use server_name as the FastMCP instance name
137+
# No existing server found - create a new local FastMCP server instance
56138
mcp = FastMCP(server_name)
57139
runtime.const_store[server_name] = mcp
58-
logger.debug(f"Created new FastMCP instance '{server_name}' for tool registration")
140+
logger.debug(f"Created new local FastMCP server instance '{server_name}' for tool registration")
141+
else:
142+
# Server already exists (could be from MCP_SERVER definition or previous TOOL)
143+
# Check if it's a Client (external) or FastMCP (local)
144+
if hasattr(mcp, 'call_tool'):
145+
# It's a FastMCP Client (external server) - cannot register local tools on it
146+
logger.warning(f"Cannot register local tools on external MCP server '{server_name}'. Tools from external servers are already available.")
147+
continue
148+
# Otherwise it's a FastMCP server instance - we can register tools on it
59149

60150
# Register each tool for this server
61151
for tool_name, tool_def in tool_list:
@@ -123,6 +213,11 @@ def compile(script: ParsedScript, runtime: RuntimeComponent = None) -> Callable:
123213
runtime.add_constants(script.constants, override=False)
124214
logger.debug(f"Initialized runtime with {len(runtime.const_store)} constants")
125215

216+
# Process MCP server definitions first (external servers)
217+
if script.mcp_servers:
218+
_register_mcp_servers(script.mcp_servers, runtime)
219+
logger.debug(f"Registered {len(script.mcp_servers)} MCP server connections")
220+
126221
# Process tool definitions and register them
127222
if script.tools:
128223
_register_tools(script.tools, runtime)

src/talkpipe/chatterlang/parsers.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class ParsedScript:
105105
pipelines: List[Union[ParsedPipeline, ParsedLoop]]
106106
constants: Dict[str, Any] = field(default_factory=dict)
107107
tools: Dict[str, Dict[str, Any]] = field(default_factory=dict)
108+
mcp_servers: Dict[str, Dict[str, Any]] = field(default_factory=dict)
108109

109110
def __iter__(self):
110111
return iter(self.pipelines)
@@ -194,6 +195,32 @@ def constant_definition():
194195
return (const_name.name, const_value)
195196
"""A parser for defining constants with 'SET' keyword."""
196197

198+
# Parser for MCP server definitions (external servers)
199+
@generate
200+
def mcp_server_definition():
201+
"""Parser for MCP_SERVER definitions: MCP_SERVER name = "url_or_config" [params]
202+
203+
Example:
204+
MCP_SERVER external_tools = "https://api.example.com/mcp" [transport="http", auth="bearer", token="token"];
205+
"""
206+
yield lexeme('MCP_SERVER')
207+
server_name = yield identifier
208+
yield lexeme('=')
209+
url_or_config = yield quoted_string # URL or config string
210+
params = yield bracket_parser # Optional parameters in brackets
211+
return (server_name.name, {
212+
'url': url_or_config,
213+
'transport': params.get('transport', 'http'), # Default to http
214+
'auth': params.get('auth', None),
215+
'token': params.get('token', None),
216+
'headers': params.get('headers', None),
217+
'command': params.get('command', None), # For stdio transport
218+
'args': params.get('args', None), # For stdio transport
219+
'cwd': params.get('cwd', None), # For stdio transport
220+
'env': params.get('env', None), # For stdio transport
221+
})
222+
"""A parser for defining external MCP server connections with 'MCP_SERVER' keyword."""
223+
197224
# Parser for tool definitions
198225
@generate
199226
def tool_definition():
@@ -371,21 +398,29 @@ def loop():
371398
pipeline_separator = lexeme(';')
372399
"""A parser for the separator between pipelines in a script."""
373400

374-
# Combined parser for constants and tools (both use semicolon separators)
375-
definition = constant_definition.map(lambda x: ('const', x)) | tool_definition.map(lambda x: ('tool', x))
401+
# Combined parser for constants, MCP servers, and tools (all use semicolon separators)
402+
definition = (
403+
constant_definition.map(lambda x: ('const', x)) |
404+
mcp_server_definition.map(lambda x: ('mcp_server', x)) |
405+
tool_definition.map(lambda x: ('tool', x))
406+
)
376407

377408
@generate
378409
def script_parser():
379-
# Parse constants and tools - they can be interleaved, separated by semicolons
410+
# Parse constants, MCP servers, and tools - they can be interleaved, separated by semicolons
380411
definitions = yield definition.sep_by(pipeline_separator, min=0)
381412

382-
# Separate constants and tools
413+
# Separate constants, MCP servers, and tools
383414
constants = {}
415+
mcp_servers = {}
384416
tools = {}
385417
for def_type, def_value in definitions:
386418
if def_type == 'const':
387419
k, v = def_value
388420
constants[k] = v
421+
elif def_type == 'mcp_server':
422+
k, v = def_value
423+
mcp_servers[k] = v
389424
else: # tool
390425
k, v = def_value
391426
tools[k] = v
@@ -395,7 +430,7 @@ def script_parser():
395430

396431
pipelines = [p for p in pipelines if not isinstance(p, ParsedPipeline) or (p.input_node or len(p.transforms)>0)]
397432

398-
# Create and return a ParsedScript with constants and tools
399-
return ParsedScript(pipelines, constants, tools)
433+
# Create and return a ParsedScript with constants, MCP servers, and tools
434+
return ParsedScript(pipelines, constants, tools, mcp_servers)
400435
"""A parser for a script in the pipeline language. Scripts contain a series of pipelines and loops."""
401436

0 commit comments

Comments
 (0)