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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-mcp = agent_mcp_framework.cli:main
@@ -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.