agent-mcp-framework 0.1.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.
- agent_mcp_framework/__init__.py +35 -0
- agent_mcp_framework/agent.py +190 -0
- agent_mcp_framework/cli.py +117 -0
- agent_mcp_framework/formatters.py +61 -0
- agent_mcp_framework/pipeline.py +238 -0
- agent_mcp_framework/py.typed +0 -0
- agent_mcp_framework/server.py +155 -0
- agent_mcp_framework-0.1.0.dist-info/METADATA +134 -0
- agent_mcp_framework-0.1.0.dist-info/RECORD +12 -0
- agent_mcp_framework-0.1.0.dist-info/WHEEL +4 -0
- agent_mcp_framework-0.1.0.dist-info/entry_points.txt +2 -0
- agent_mcp_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""agent-mcp-framework: Build multi-agent MCP servers in Python."""
|
|
2
|
+
|
|
3
|
+
from agent_mcp_framework.agent import (
|
|
4
|
+
Agent,
|
|
5
|
+
AgentContext,
|
|
6
|
+
AgentResult,
|
|
7
|
+
FunctionAgent,
|
|
8
|
+
LLMAgent,
|
|
9
|
+
)
|
|
10
|
+
from agent_mcp_framework.pipeline import (
|
|
11
|
+
ConditionalPipeline,
|
|
12
|
+
MapReducePipeline,
|
|
13
|
+
ParallelPipeline,
|
|
14
|
+
Pipeline,
|
|
15
|
+
PipelineResult,
|
|
16
|
+
SequentialPipeline,
|
|
17
|
+
)
|
|
18
|
+
from agent_mcp_framework.server import AgentMCPServer
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Agent",
|
|
24
|
+
"AgentContext",
|
|
25
|
+
"AgentResult",
|
|
26
|
+
"AgentMCPServer",
|
|
27
|
+
"ConditionalPipeline",
|
|
28
|
+
"FunctionAgent",
|
|
29
|
+
"LLMAgent",
|
|
30
|
+
"MapReducePipeline",
|
|
31
|
+
"ParallelPipeline",
|
|
32
|
+
"Pipeline",
|
|
33
|
+
"PipelineResult",
|
|
34
|
+
"SequentialPipeline",
|
|
35
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Core Agent abstraction for multi-agent systems."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentStatus(str, Enum):
|
|
15
|
+
IDLE = "idle"
|
|
16
|
+
RUNNING = "running"
|
|
17
|
+
COMPLETED = "completed"
|
|
18
|
+
FAILED = "failed"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentContext(BaseModel):
|
|
22
|
+
"""Shared context passed between agents in a pipeline."""
|
|
23
|
+
|
|
24
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
+
errors: list[str] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
29
|
+
return self.data.get(key, default)
|
|
30
|
+
|
|
31
|
+
def set(self, key: str, value: Any) -> None:
|
|
32
|
+
self.data[key] = value
|
|
33
|
+
|
|
34
|
+
def merge(self, other: AgentContext) -> AgentContext:
|
|
35
|
+
"""Merge another context into this one, returning a new context."""
|
|
36
|
+
merged_data = {**self.data, **other.data}
|
|
37
|
+
merged_metadata = {**self.metadata, **other.metadata}
|
|
38
|
+
merged_errors = self.errors + other.errors
|
|
39
|
+
return AgentContext(data=merged_data, metadata=merged_metadata, errors=merged_errors)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AgentResult(BaseModel):
|
|
43
|
+
"""Result returned by an agent after execution."""
|
|
44
|
+
|
|
45
|
+
success: bool
|
|
46
|
+
output: Any = None
|
|
47
|
+
error: str | None = None
|
|
48
|
+
duration_ms: float = 0.0
|
|
49
|
+
agent_name: str = ""
|
|
50
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Agent(ABC):
|
|
54
|
+
"""Base class for all agents.
|
|
55
|
+
|
|
56
|
+
Subclass this and implement `run()` to create an agent.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
class MyAgent(Agent):
|
|
60
|
+
async def run(self, context: AgentContext) -> AgentResult:
|
|
61
|
+
data = context.get("input")
|
|
62
|
+
result = process(data)
|
|
63
|
+
context.set("output", result)
|
|
64
|
+
return AgentResult(success=True, output=result, agent_name=self.name)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, name: str, description: str = ""):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.description = description
|
|
70
|
+
self.status = AgentStatus.IDLE
|
|
71
|
+
self._hooks: dict[str, list] = {
|
|
72
|
+
"before_run": [],
|
|
73
|
+
"after_run": [],
|
|
74
|
+
"on_error": [],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def on(self, event: str, callback) -> Agent:
|
|
78
|
+
"""Register a lifecycle hook."""
|
|
79
|
+
if event not in self._hooks:
|
|
80
|
+
raise ValueError(f"Unknown event: {event}. Valid: {list(self._hooks.keys())}")
|
|
81
|
+
self._hooks[event].append(callback)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def _fire(self, event: str, **kwargs) -> None:
|
|
85
|
+
for cb in self._hooks.get(event, []):
|
|
86
|
+
result = cb(**kwargs)
|
|
87
|
+
if asyncio.iscoroutine(result):
|
|
88
|
+
await result
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
async def run(self, context: AgentContext) -> AgentResult:
|
|
92
|
+
"""Execute the agent's logic. Must be implemented by subclasses."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
async def execute(self, context: AgentContext) -> AgentResult:
|
|
96
|
+
"""Run the agent with lifecycle hooks and error handling."""
|
|
97
|
+
self.status = AgentStatus.RUNNING
|
|
98
|
+
start = time.monotonic()
|
|
99
|
+
try:
|
|
100
|
+
await self._fire("before_run", agent=self, context=context)
|
|
101
|
+
result = await self.run(context)
|
|
102
|
+
result.agent_name = self.name
|
|
103
|
+
result.duration_ms = (time.monotonic() - start) * 1000
|
|
104
|
+
self.status = AgentStatus.COMPLETED
|
|
105
|
+
await self._fire("after_run", agent=self, context=context, result=result)
|
|
106
|
+
return result
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.status = AgentStatus.FAILED
|
|
109
|
+
duration = (time.monotonic() - start) * 1000
|
|
110
|
+
error_result = AgentResult(
|
|
111
|
+
success=False,
|
|
112
|
+
error=str(e),
|
|
113
|
+
agent_name=self.name,
|
|
114
|
+
duration_ms=duration,
|
|
115
|
+
)
|
|
116
|
+
context.errors.append(f"{self.name}: {e}")
|
|
117
|
+
await self._fire("on_error", agent=self, context=context, error=e)
|
|
118
|
+
return error_result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class LLMAgent(Agent):
|
|
122
|
+
"""Agent that uses an LLM (Claude) for processing.
|
|
123
|
+
|
|
124
|
+
Subclass this for agents that need LLM capabilities. Provides
|
|
125
|
+
a configured Anthropic client and helper methods.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
name: str,
|
|
131
|
+
description: str = "",
|
|
132
|
+
model: str = "claude-sonnet-4-5-20250929",
|
|
133
|
+
system_prompt: str = "",
|
|
134
|
+
max_tokens: int = 4096,
|
|
135
|
+
):
|
|
136
|
+
super().__init__(name, description)
|
|
137
|
+
self.model = model
|
|
138
|
+
self.system_prompt = system_prompt
|
|
139
|
+
self.max_tokens = max_tokens
|
|
140
|
+
self._client = None
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def client(self):
|
|
144
|
+
if self._client is None:
|
|
145
|
+
import anthropic
|
|
146
|
+
self._client = anthropic.AsyncAnthropic()
|
|
147
|
+
return self._client
|
|
148
|
+
|
|
149
|
+
async def complete(self, prompt: str, **kwargs) -> str:
|
|
150
|
+
"""Send a completion request to Claude."""
|
|
151
|
+
messages = [{"role": "user", "content": prompt}]
|
|
152
|
+
params = {
|
|
153
|
+
"model": kwargs.pop("model", self.model),
|
|
154
|
+
"max_tokens": kwargs.pop("max_tokens", self.max_tokens),
|
|
155
|
+
"messages": messages,
|
|
156
|
+
**kwargs,
|
|
157
|
+
}
|
|
158
|
+
if self.system_prompt:
|
|
159
|
+
params["system"] = self.system_prompt
|
|
160
|
+
response = await self.client.messages.create(**params)
|
|
161
|
+
if not response.content:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Empty response from {params['model']} "
|
|
164
|
+
f"(stop_reason: {response.stop_reason})"
|
|
165
|
+
)
|
|
166
|
+
return response.content[0].text
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class FunctionAgent(Agent):
|
|
170
|
+
"""Agent created from a plain function.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
async def analyze(ctx: AgentContext) -> AgentResult:
|
|
174
|
+
data = ctx.get("input")
|
|
175
|
+
return AgentResult(success=True, output=len(data))
|
|
176
|
+
|
|
177
|
+
agent = FunctionAgent("counter", fn=analyze)
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, name: str, fn, description: str = ""):
|
|
181
|
+
super().__init__(name, description or f"Function agent: {fn.__name__}")
|
|
182
|
+
self._fn = fn
|
|
183
|
+
|
|
184
|
+
async def run(self, context: AgentContext) -> AgentResult:
|
|
185
|
+
result = self._fn(context)
|
|
186
|
+
if asyncio.iscoroutine(result):
|
|
187
|
+
result = await result
|
|
188
|
+
if isinstance(result, AgentResult):
|
|
189
|
+
return result
|
|
190
|
+
return AgentResult(success=True, output=result, agent_name=self.name)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""CLI interface for running agent-mcp-framework servers and pipelines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from agent_mcp_framework import __version__
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
@click.version_option(version=__version__)
|
|
20
|
+
def main():
|
|
21
|
+
"""agent-mcp-framework: Build multi-agent MCP servers in Python."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@main.command()
|
|
26
|
+
@click.argument("module_path")
|
|
27
|
+
@click.option("--transport", "-t", default="stdio", type=click.Choice(["stdio", "sse"]))
|
|
28
|
+
def serve(module_path: str, transport: str):
|
|
29
|
+
"""Start an MCP server from a Python module.
|
|
30
|
+
|
|
31
|
+
MODULE_PATH should be a dotted path to a module containing an AgentMCPServer
|
|
32
|
+
instance named 'server' (e.g., 'my_project.server').
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
mod = importlib.import_module(module_path)
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
console.print(f"[red]Error:[/red] Could not import '{module_path}': {e}")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
server = getattr(mod, "server", None)
|
|
41
|
+
if server is None:
|
|
42
|
+
console.print(
|
|
43
|
+
f"[red]Error:[/red] Module '{module_path}' has no 'server' attribute. "
|
|
44
|
+
"Define an AgentMCPServer instance named 'server'."
|
|
45
|
+
)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
console.print(f"[green]Starting MCP server[/green] '{server.name}' via {transport}")
|
|
49
|
+
server.run(transport=transport)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@main.command()
|
|
53
|
+
@click.argument("module_path")
|
|
54
|
+
@click.option("--input", "-i", "input_data", default=None, help="JSON input data")
|
|
55
|
+
@click.option(
|
|
56
|
+
"--format", "-f", "output_format",
|
|
57
|
+
default="text",
|
|
58
|
+
type=click.Choice(["json", "markdown", "text"]),
|
|
59
|
+
)
|
|
60
|
+
def run(module_path: str, input_data: str | None, output_format: str):
|
|
61
|
+
"""Run a pipeline directly from a Python module.
|
|
62
|
+
|
|
63
|
+
MODULE_PATH should be a dotted path to a module containing a Pipeline
|
|
64
|
+
instance named 'pipeline' (e.g., 'my_project.pipeline').
|
|
65
|
+
"""
|
|
66
|
+
import json as json_mod
|
|
67
|
+
|
|
68
|
+
from agent_mcp_framework.agent import AgentContext
|
|
69
|
+
from agent_mcp_framework.server import format_pipeline_result
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
mod = importlib.import_module(module_path)
|
|
73
|
+
except ImportError as e:
|
|
74
|
+
console.print(f"[red]Error:[/red] Could not import '{module_path}': {e}")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
pipeline = getattr(mod, "pipeline", None)
|
|
78
|
+
if pipeline is None:
|
|
79
|
+
console.print(
|
|
80
|
+
f"[red]Error:[/red] Module '{module_path}' has no 'pipeline' attribute."
|
|
81
|
+
)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
ctx = AgentContext()
|
|
85
|
+
if input_data:
|
|
86
|
+
try:
|
|
87
|
+
ctx.data = json_mod.loads(input_data)
|
|
88
|
+
except json_mod.JSONDecodeError as e:
|
|
89
|
+
console.print(f"[red]Error:[/red] Invalid JSON input: {e}")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
result = asyncio.run(pipeline.execute(ctx))
|
|
93
|
+
output = format_pipeline_result(result, output_format)
|
|
94
|
+
console.print(output)
|
|
95
|
+
|
|
96
|
+
if not result.success:
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@main.command()
|
|
101
|
+
def info():
|
|
102
|
+
"""Show framework version and capabilities."""
|
|
103
|
+
table = Table(title="agent-mcp-framework")
|
|
104
|
+
table.add_column("Property", style="cyan")
|
|
105
|
+
table.add_column("Value", style="green")
|
|
106
|
+
|
|
107
|
+
table.add_row("Version", __version__)
|
|
108
|
+
table.add_row("Agents", "Agent, LLMAgent, FunctionAgent")
|
|
109
|
+
table.add_row("Pipelines", "Sequential, Parallel, Conditional, MapReduce")
|
|
110
|
+
table.add_row("Transports", "stdio, SSE")
|
|
111
|
+
table.add_row("Output Formats", "JSON, Markdown, Text")
|
|
112
|
+
|
|
113
|
+
console.print(table)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Output formatting for agent results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from agent_mcp_framework.agent import AgentResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_result(result: AgentResult, fmt: str = "json") -> str:
|
|
11
|
+
"""Format an agent result for output."""
|
|
12
|
+
if fmt == "json":
|
|
13
|
+
return _format_json(result)
|
|
14
|
+
elif fmt == "markdown":
|
|
15
|
+
return _format_markdown(result)
|
|
16
|
+
elif fmt == "text":
|
|
17
|
+
return _format_text(result)
|
|
18
|
+
else:
|
|
19
|
+
raise ValueError(f"Unknown format: {fmt}. Valid: json, markdown, text")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _format_json(result: AgentResult) -> str:
|
|
23
|
+
data = {
|
|
24
|
+
"success": result.success,
|
|
25
|
+
"agent": result.agent_name,
|
|
26
|
+
"duration_ms": round(result.duration_ms, 2),
|
|
27
|
+
}
|
|
28
|
+
if result.output is not None:
|
|
29
|
+
data["output"] = result.output
|
|
30
|
+
if result.error:
|
|
31
|
+
data["error"] = result.error
|
|
32
|
+
if result.metadata:
|
|
33
|
+
data["metadata"] = result.metadata
|
|
34
|
+
return json.dumps(data, indent=2, default=str)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_markdown(result: AgentResult) -> str:
|
|
38
|
+
icon = "+" if result.success else "x"
|
|
39
|
+
lines = [
|
|
40
|
+
f"## [{icon}] {result.agent_name}",
|
|
41
|
+
f"**Duration:** {result.duration_ms:.0f}ms",
|
|
42
|
+
"",
|
|
43
|
+
]
|
|
44
|
+
if result.output is not None:
|
|
45
|
+
output = result.output
|
|
46
|
+
if isinstance(output, (dict, list)):
|
|
47
|
+
output = json.dumps(output, indent=2, default=str)
|
|
48
|
+
lines.append(f"```\n{output}\n```")
|
|
49
|
+
if result.error:
|
|
50
|
+
lines.append(f"**Error:** {result.error}")
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_text(result: AgentResult) -> str:
|
|
55
|
+
status = "OK" if result.success else "FAIL"
|
|
56
|
+
parts = [f"[{status}] {result.agent_name} ({result.duration_ms:.0f}ms)"]
|
|
57
|
+
if result.output is not None:
|
|
58
|
+
parts.append(str(result.output))
|
|
59
|
+
if result.error:
|
|
60
|
+
parts.append(f"Error: {result.error}")
|
|
61
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Pipeline composition patterns for multi-agent workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from agent_mcp_framework.agent import Agent, AgentContext, AgentResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PipelineResult(BaseModel):
|
|
16
|
+
"""Aggregated result from a pipeline execution."""
|
|
17
|
+
|
|
18
|
+
success: bool
|
|
19
|
+
results: list[AgentResult] = Field(default_factory=list)
|
|
20
|
+
duration_ms: float = 0.0
|
|
21
|
+
pipeline_name: str = ""
|
|
22
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def failed(self) -> list[AgentResult]:
|
|
26
|
+
return [r for r in self.results if not r.success]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def outputs(self) -> dict[str, Any]:
|
|
30
|
+
return {r.agent_name: r.output for r in self.results}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Pipeline(ABC):
|
|
34
|
+
"""Base class for agent composition patterns."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, name: str, agents: list[Agent] | None = None):
|
|
37
|
+
self.name = name
|
|
38
|
+
self.agents: list[Agent] = agents or []
|
|
39
|
+
|
|
40
|
+
def add(self, agent: Agent) -> Pipeline:
|
|
41
|
+
"""Add an agent to the pipeline."""
|
|
42
|
+
self.agents.append(agent)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def execute(self, context: AgentContext | None = None) -> PipelineResult:
|
|
47
|
+
"""Execute the pipeline. Must be implemented by subclasses."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SequentialPipeline(Pipeline):
|
|
52
|
+
"""Execute agents one after another. Each agent sees the updated context.
|
|
53
|
+
|
|
54
|
+
If `stop_on_failure` is True (default), the pipeline stops when any agent fails.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self, name: str, agents: list[Agent] | None = None, stop_on_failure: bool = True
|
|
59
|
+
):
|
|
60
|
+
super().__init__(name, agents)
|
|
61
|
+
self.stop_on_failure = stop_on_failure
|
|
62
|
+
|
|
63
|
+
async def execute(self, context: AgentContext | None = None) -> PipelineResult:
|
|
64
|
+
ctx = context or AgentContext()
|
|
65
|
+
results: list[AgentResult] = []
|
|
66
|
+
start = time.monotonic()
|
|
67
|
+
|
|
68
|
+
for agent in self.agents:
|
|
69
|
+
result = await agent.execute(ctx)
|
|
70
|
+
results.append(result)
|
|
71
|
+
if not result.success and self.stop_on_failure:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
duration = (time.monotonic() - start) * 1000
|
|
75
|
+
all_ok = all(r.success for r in results)
|
|
76
|
+
return PipelineResult(
|
|
77
|
+
success=all_ok,
|
|
78
|
+
results=results,
|
|
79
|
+
duration_ms=duration,
|
|
80
|
+
pipeline_name=self.name,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ParallelPipeline(Pipeline):
|
|
85
|
+
"""Execute agents concurrently with isolated contexts.
|
|
86
|
+
|
|
87
|
+
Each agent receives a snapshot of the input context to prevent race
|
|
88
|
+
conditions on shared mutable state. After all agents complete, their
|
|
89
|
+
contexts are merged back into the original.
|
|
90
|
+
|
|
91
|
+
If `max_concurrency` is set, limits how many agents run simultaneously.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
name: str,
|
|
97
|
+
agents: list[Agent] | None = None,
|
|
98
|
+
max_concurrency: int | None = None,
|
|
99
|
+
):
|
|
100
|
+
super().__init__(name, agents)
|
|
101
|
+
self.max_concurrency = max_concurrency
|
|
102
|
+
|
|
103
|
+
async def execute(self, context: AgentContext | None = None) -> PipelineResult:
|
|
104
|
+
ctx = context or AgentContext()
|
|
105
|
+
start = time.monotonic()
|
|
106
|
+
|
|
107
|
+
# Each agent gets an isolated copy to prevent race conditions
|
|
108
|
+
snapshots = [ctx.model_copy(deep=True) for _ in self.agents]
|
|
109
|
+
|
|
110
|
+
if self.max_concurrency:
|
|
111
|
+
sem = asyncio.Semaphore(self.max_concurrency)
|
|
112
|
+
|
|
113
|
+
async def run_with_sem(agent: Agent, snap: AgentContext) -> AgentResult:
|
|
114
|
+
async with sem:
|
|
115
|
+
return await agent.execute(snap)
|
|
116
|
+
|
|
117
|
+
results = await asyncio.gather(
|
|
118
|
+
*[run_with_sem(a, s) for a, s in zip(self.agents, snapshots)]
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
results = await asyncio.gather(
|
|
122
|
+
*[a.execute(s) for a, s in zip(self.agents, snapshots)]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Merge snapshot data back into the original context
|
|
126
|
+
for snap in snapshots:
|
|
127
|
+
ctx.data.update(snap.data)
|
|
128
|
+
ctx.errors.extend(e for e in snap.errors if e not in ctx.errors)
|
|
129
|
+
|
|
130
|
+
results = list(results)
|
|
131
|
+
duration = (time.monotonic() - start) * 1000
|
|
132
|
+
all_ok = all(r.success for r in results)
|
|
133
|
+
return PipelineResult(
|
|
134
|
+
success=all_ok,
|
|
135
|
+
results=results,
|
|
136
|
+
duration_ms=duration,
|
|
137
|
+
pipeline_name=self.name,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ConditionalPipeline(Pipeline):
|
|
142
|
+
"""Route to different agents based on a condition function.
|
|
143
|
+
|
|
144
|
+
The `router` function receives the context and returns the name of the
|
|
145
|
+
agent to execute, or None to skip.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
name: str,
|
|
151
|
+
agents: list[Agent] | None = None,
|
|
152
|
+
router: Callable[[AgentContext], str | None] = None,
|
|
153
|
+
):
|
|
154
|
+
super().__init__(name, agents)
|
|
155
|
+
self.router = router
|
|
156
|
+
|
|
157
|
+
def _agent_map(self) -> dict[str, Agent]:
|
|
158
|
+
return {a.name: a for a in self.agents}
|
|
159
|
+
|
|
160
|
+
async def execute(self, context: AgentContext | None = None) -> PipelineResult:
|
|
161
|
+
ctx = context or AgentContext()
|
|
162
|
+
start = time.monotonic()
|
|
163
|
+
|
|
164
|
+
if self.router is None:
|
|
165
|
+
raise ValueError("ConditionalPipeline requires a router function")
|
|
166
|
+
|
|
167
|
+
target_name = self.router(ctx)
|
|
168
|
+
results: list[AgentResult] = []
|
|
169
|
+
|
|
170
|
+
if target_name is not None:
|
|
171
|
+
agent_map = self._agent_map()
|
|
172
|
+
if target_name not in agent_map:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Router returned '{target_name}' but no agent with that name exists. "
|
|
175
|
+
f"Available: {list(agent_map.keys())}"
|
|
176
|
+
)
|
|
177
|
+
result = await agent_map[target_name].execute(ctx)
|
|
178
|
+
results.append(result)
|
|
179
|
+
|
|
180
|
+
duration = (time.monotonic() - start) * 1000
|
|
181
|
+
all_ok = all(r.success for r in results) if results else True
|
|
182
|
+
return PipelineResult(
|
|
183
|
+
success=all_ok,
|
|
184
|
+
results=results,
|
|
185
|
+
duration_ms=duration,
|
|
186
|
+
pipeline_name=self.name,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MapReducePipeline(Pipeline):
|
|
191
|
+
"""Fan-out work across agents, then reduce results.
|
|
192
|
+
|
|
193
|
+
The `splitter` function breaks the context into N sub-contexts (one per agent).
|
|
194
|
+
The `reducer` function combines all results into the final context.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
name: str,
|
|
200
|
+
agents: list[Agent] | None = None,
|
|
201
|
+
splitter: Callable[[AgentContext], list[AgentContext]] | None = None,
|
|
202
|
+
reducer: Callable[[list[AgentResult], AgentContext], None] | None = None,
|
|
203
|
+
):
|
|
204
|
+
super().__init__(name, agents)
|
|
205
|
+
self.splitter = splitter
|
|
206
|
+
self.reducer = reducer
|
|
207
|
+
|
|
208
|
+
async def execute(self, context: AgentContext | None = None) -> PipelineResult:
|
|
209
|
+
ctx = context or AgentContext()
|
|
210
|
+
start = time.monotonic()
|
|
211
|
+
|
|
212
|
+
if self.splitter is None:
|
|
213
|
+
raise ValueError("MapReducePipeline requires a splitter function")
|
|
214
|
+
|
|
215
|
+
sub_contexts = self.splitter(ctx)
|
|
216
|
+
|
|
217
|
+
if len(sub_contexts) != len(self.agents):
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Splitter returned {len(sub_contexts)} contexts but pipeline "
|
|
220
|
+
f"has {len(self.agents)} agents"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
results = await asyncio.gather(
|
|
224
|
+
*[agent.execute(sub_ctx) for agent, sub_ctx in zip(self.agents, sub_contexts)]
|
|
225
|
+
)
|
|
226
|
+
results = list(results)
|
|
227
|
+
|
|
228
|
+
if self.reducer:
|
|
229
|
+
self.reducer(results, ctx)
|
|
230
|
+
|
|
231
|
+
duration = (time.monotonic() - start) * 1000
|
|
232
|
+
all_ok = all(r.success for r in results)
|
|
233
|
+
return PipelineResult(
|
|
234
|
+
success=all_ok,
|
|
235
|
+
results=results,
|
|
236
|
+
duration_ms=duration,
|
|
237
|
+
pipeline_name=self.name,
|
|
238
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""MCP Server that exposes agent pipelines as tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from agent_mcp_framework.agent import Agent, AgentContext
|
|
11
|
+
from agent_mcp_framework.formatters import format_result
|
|
12
|
+
from agent_mcp_framework.pipeline import Pipeline, PipelineResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentMCPServer:
|
|
16
|
+
"""Wraps agent pipelines as MCP tools served over stdio/SSE.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
server = AgentMCPServer("my-server", description="Code analysis tools")
|
|
20
|
+
server.add_agent_tool(
|
|
21
|
+
my_agent,
|
|
22
|
+
name="analyze",
|
|
23
|
+
description="Analyze code quality",
|
|
24
|
+
)
|
|
25
|
+
server.run()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str, description: str = ""):
|
|
29
|
+
self.name = name
|
|
30
|
+
self.description = description
|
|
31
|
+
self._mcp = FastMCP(name)
|
|
32
|
+
self._agents: dict[str, Agent] = {}
|
|
33
|
+
self._pipelines: dict[str, Pipeline] = {}
|
|
34
|
+
|
|
35
|
+
def add_agent_tool(
|
|
36
|
+
self,
|
|
37
|
+
agent: Agent,
|
|
38
|
+
name: str | None = None,
|
|
39
|
+
description: str | None = None,
|
|
40
|
+
context_mapper: Callable[[dict[str, Any]], AgentContext] | None = None,
|
|
41
|
+
output_format: str = "json",
|
|
42
|
+
) -> AgentMCPServer:
|
|
43
|
+
"""Register an agent as an MCP tool.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
agent: The agent to expose as a tool.
|
|
47
|
+
name: Tool name (defaults to agent.name).
|
|
48
|
+
description: Tool description (defaults to agent.description).
|
|
49
|
+
context_mapper: Function to convert tool params into an AgentContext.
|
|
50
|
+
output_format: How to format the result ("json", "markdown", "text").
|
|
51
|
+
"""
|
|
52
|
+
tool_name = name or agent.name
|
|
53
|
+
tool_desc = description or agent.description
|
|
54
|
+
self._agents[tool_name] = agent
|
|
55
|
+
mapper = context_mapper or _default_context_mapper
|
|
56
|
+
|
|
57
|
+
async def tool_handler(**kwargs) -> str:
|
|
58
|
+
ctx = mapper(kwargs)
|
|
59
|
+
result = await agent.execute(ctx)
|
|
60
|
+
return format_result(result, output_format)
|
|
61
|
+
|
|
62
|
+
# Register with FastMCP
|
|
63
|
+
self._mcp.add_tool(
|
|
64
|
+
tool_handler,
|
|
65
|
+
name=tool_name,
|
|
66
|
+
description=tool_desc,
|
|
67
|
+
)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def add_pipeline_tool(
|
|
71
|
+
self,
|
|
72
|
+
pipeline: Pipeline,
|
|
73
|
+
name: str | None = None,
|
|
74
|
+
description: str = "",
|
|
75
|
+
context_mapper: Callable[[dict[str, Any]], AgentContext] | None = None,
|
|
76
|
+
output_format: str = "json",
|
|
77
|
+
) -> AgentMCPServer:
|
|
78
|
+
"""Register a pipeline as an MCP tool."""
|
|
79
|
+
tool_name = name or pipeline.name
|
|
80
|
+
self._pipelines[tool_name] = pipeline
|
|
81
|
+
|
|
82
|
+
mapper = context_mapper or _default_context_mapper
|
|
83
|
+
|
|
84
|
+
async def pipeline_handler(**kwargs) -> str:
|
|
85
|
+
ctx = mapper(kwargs)
|
|
86
|
+
result = await pipeline.execute(ctx)
|
|
87
|
+
return format_pipeline_result(result, output_format)
|
|
88
|
+
|
|
89
|
+
self._mcp.add_tool(
|
|
90
|
+
pipeline_handler,
|
|
91
|
+
name=tool_name,
|
|
92
|
+
description=description,
|
|
93
|
+
)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def run(self, transport: str = "stdio") -> None:
|
|
97
|
+
"""Start the MCP server."""
|
|
98
|
+
self._mcp.run(transport=transport)
|
|
99
|
+
|
|
100
|
+
async def run_async(self, transport: str = "stdio") -> None:
|
|
101
|
+
"""Start the MCP server asynchronously."""
|
|
102
|
+
if transport == "stdio":
|
|
103
|
+
await self._mcp.run_stdio_async()
|
|
104
|
+
elif transport == "sse":
|
|
105
|
+
await self._mcp.run_sse_async()
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(f"Unknown transport: {transport}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _default_context_mapper(params: dict[str, Any]) -> AgentContext:
|
|
111
|
+
"""Default mapping: put all params into context.data."""
|
|
112
|
+
return AgentContext(data=params)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_pipeline_result(result: PipelineResult, fmt: str = "json") -> str:
|
|
116
|
+
"""Format a pipeline result for MCP output."""
|
|
117
|
+
if fmt == "json":
|
|
118
|
+
return json.dumps(
|
|
119
|
+
{
|
|
120
|
+
"success": result.success,
|
|
121
|
+
"pipeline": result.pipeline_name,
|
|
122
|
+
"duration_ms": round(result.duration_ms, 2),
|
|
123
|
+
"results": [
|
|
124
|
+
{
|
|
125
|
+
"agent": r.agent_name,
|
|
126
|
+
"success": r.success,
|
|
127
|
+
"output": r.output,
|
|
128
|
+
"error": r.error,
|
|
129
|
+
"duration_ms": round(r.duration_ms, 2),
|
|
130
|
+
}
|
|
131
|
+
for r in result.results
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
indent=2,
|
|
135
|
+
default=str,
|
|
136
|
+
)
|
|
137
|
+
elif fmt == "markdown":
|
|
138
|
+
lines = [f"## Pipeline: {result.pipeline_name}"]
|
|
139
|
+
lines.append(f"**Status:** {'Success' if result.success else 'Failed'}")
|
|
140
|
+
lines.append(f"**Duration:** {result.duration_ms:.0f}ms\n")
|
|
141
|
+
for r in result.results:
|
|
142
|
+
icon = "+" if r.success else "x"
|
|
143
|
+
lines.append(f"### [{icon}] {r.agent_name} ({r.duration_ms:.0f}ms)")
|
|
144
|
+
if r.output is not None:
|
|
145
|
+
lines.append(f"```\n{r.output}\n```")
|
|
146
|
+
if r.error:
|
|
147
|
+
lines.append(f"**Error:** {r.error}")
|
|
148
|
+
lines.append("")
|
|
149
|
+
return "\n".join(lines)
|
|
150
|
+
else:
|
|
151
|
+
parts = [f"Pipeline '{result.pipeline_name}': {'OK' if result.success else 'FAILED'}"]
|
|
152
|
+
for r in result.results:
|
|
153
|
+
status = "OK" if r.success else "FAIL"
|
|
154
|
+
parts.append(f" [{status}] {r.agent_name}: {r.output or r.error}")
|
|
155
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-mcp-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python framework for building multi-agent MCP servers
|
|
5
|
+
Project-URL: Homepage, https://github.com/bifrostlabs/agent-mcp-framework
|
|
6
|
+
Project-URL: Documentation, https://github.com/bifrostlabs/agent-mcp-framework#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/bifrostlabs/agent-mcp-framework
|
|
8
|
+
Project-URL: Issues, https://github.com/bifrostlabs/agent-mcp-framework/issues
|
|
9
|
+
Author-email: Jarrad Bermingham <jarrad@bifrostlabs.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,llm,mcp,model-context-protocol,multi-agent
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: anthropic>=0.40.0
|
|
25
|
+
Requires-Dist: click>=8.0.0
|
|
26
|
+
Requires-Dist: mcp>=1.0.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0.0
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# agent-mcp-framework
|
|
37
|
+
|
|
38
|
+
A Python framework for building multi-agent MCP (Model Context Protocol) servers.
|
|
39
|
+
|
|
40
|
+
Build production-ready multi-agent systems that expose their capabilities as MCP tools — ready to integrate with Claude, VSCode, and any MCP-compatible client.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Agent abstractions** — `Agent`, `LLMAgent`, `FunctionAgent` with lifecycle hooks
|
|
45
|
+
- **Pipeline composition** — Sequential, Parallel, Conditional, and MapReduce patterns
|
|
46
|
+
- **MCP integration** — Expose agent pipelines as MCP tools over stdio or SSE
|
|
47
|
+
- **Output formatting** — JSON, Markdown, and plain text output modes
|
|
48
|
+
- **CLI** — Run servers and pipelines from the command line
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install agent-mcp-framework
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from agent_mcp_framework import Agent, AgentContext, AgentResult, SequentialPipeline, AgentMCPServer
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AnalyzerAgent(Agent):
|
|
63
|
+
async def run(self, context: AgentContext) -> AgentResult:
|
|
64
|
+
code = context.get("code", "")
|
|
65
|
+
issues = []
|
|
66
|
+
if len(code.splitlines()) > 500:
|
|
67
|
+
issues.append("File exceeds 500 lines — consider splitting")
|
|
68
|
+
if "import *" in code:
|
|
69
|
+
issues.append("Wildcard imports detected")
|
|
70
|
+
context.set("issues", issues)
|
|
71
|
+
return AgentResult(success=True, output={"issues": issues, "count": len(issues)})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ScorerAgent(Agent):
|
|
75
|
+
async def run(self, context: AgentContext) -> AgentResult:
|
|
76
|
+
issues = context.get("issues", [])
|
|
77
|
+
score = max(0, 100 - len(issues) * 15)
|
|
78
|
+
return AgentResult(success=True, output={"score": score, "grade": "A" if score >= 90 else "B" if score >= 70 else "C"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Compose agents into a pipeline
|
|
82
|
+
pipeline = SequentialPipeline("code-review", agents=[
|
|
83
|
+
AnalyzerAgent("analyzer", description="Find code issues"),
|
|
84
|
+
ScorerAgent("scorer", description="Score code quality"),
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
# Expose as an MCP server
|
|
88
|
+
server = AgentMCPServer("code-review-server", description="Multi-agent code review")
|
|
89
|
+
server.add_pipeline_tool(
|
|
90
|
+
pipeline,
|
|
91
|
+
name="review_code",
|
|
92
|
+
description="Analyze code quality and return a score",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
server.run() # Starts MCP server on stdio
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Agent Types
|
|
100
|
+
|
|
101
|
+
### `Agent` — Base class
|
|
102
|
+
Implement `run()` to define your agent's logic.
|
|
103
|
+
|
|
104
|
+
### `LLMAgent` — Claude-powered agent
|
|
105
|
+
Built-in Anthropic client with `complete()` helper for LLM calls.
|
|
106
|
+
|
|
107
|
+
### `FunctionAgent` — Quick inline agents
|
|
108
|
+
Wrap any async function as an agent without subclassing.
|
|
109
|
+
|
|
110
|
+
## Pipeline Patterns
|
|
111
|
+
|
|
112
|
+
| Pattern | Description |
|
|
113
|
+
|---------|-------------|
|
|
114
|
+
| `SequentialPipeline` | Run agents one after another, each seeing updated context |
|
|
115
|
+
| `ParallelPipeline` | Run agents concurrently with optional concurrency limits |
|
|
116
|
+
| `ConditionalPipeline` | Route to agents based on a condition function |
|
|
117
|
+
| `MapReducePipeline` | Split work, fan out, and reduce results |
|
|
118
|
+
|
|
119
|
+
## CLI
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Start an MCP server
|
|
123
|
+
agent-mcp serve my_project.server
|
|
124
|
+
|
|
125
|
+
# Run a pipeline directly
|
|
126
|
+
agent-mcp run my_project.pipeline --input '{"code": "import *"}'
|
|
127
|
+
|
|
128
|
+
# Show framework info
|
|
129
|
+
agent-mcp info
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
agent_mcp_framework/__init__.py,sha256=O4RglxX_Z6niquDMIRH-w4AP1UCaWOuptsv4A573Hnk,706
|
|
2
|
+
agent_mcp_framework/agent.py,sha256=EgGZBRcRvtSaf34hImb6uyDQBHdLIIGx9gkmXq-o2ts,6313
|
|
3
|
+
agent_mcp_framework/cli.py,sha256=3Ur6RoN_fNbLSQ6ml0ys7Y6Fpu53sOCRmBBPu04H5V4,3502
|
|
4
|
+
agent_mcp_framework/formatters.py,sha256=PQZtBUfkNYNDqhHKVYxBM84WZq25CHN43dAu-tG9U2s,1856
|
|
5
|
+
agent_mcp_framework/pipeline.py,sha256=s-aerkmcVh-QLiDVa59bZh5l94nOFxV-TN-LCO217hc,7712
|
|
6
|
+
agent_mcp_framework/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
agent_mcp_framework/server.py,sha256=i8H93qLIuyKaPRepEXsEfbNBzbtxNMTpvawXGwRvS_4,5414
|
|
8
|
+
agent_mcp_framework-0.1.0.dist-info/METADATA,sha256=54-L4DnAUy06oChebbpFnk2W8F9LrHDotUnZCLB1w2g,4665
|
|
9
|
+
agent_mcp_framework-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
agent_mcp_framework-0.1.0.dist-info/entry_points.txt,sha256=viNuRjlGdG6WC0pWF6Ts41TZPMPGcVN2menoNKfYKEY,59
|
|
11
|
+
agent_mcp_framework-0.1.0.dist-info/licenses/LICENSE,sha256=pxGZ-WmjWu8N3FUJi6lu7o-p1fW3m5TqAR5SWn1HvIg,1089
|
|
12
|
+
agent_mcp_framework-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jarrad Bermingham / Bifrost Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|