Skip to content

Expose mcp-agent apps (MCPApp) as MCP servers #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/mcp_basic_slack_agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

app = MCPApp(name="mcp_basic_agent")


async def example_usage():
async with app.run() as agent_app:
logger = agent_app.logger
Expand Down
7 changes: 5 additions & 2 deletions examples/mcp_hello_world/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ async def example_usage():

try:
filesystem_client = await connection_manager.get_server(
server_name="filesystem", client_session_factory=MCPAgentClientSession
server_name="filesystem",
client_session_factory=MCPAgentClientSession,
)
logger.info(
"filesystem: Connected to server with persistent connection."
)
logger.info("filesystem: Connected to server with persistent connection.")

fetch_client = await connection_manager.get_server(
server_name="fetch", client_session_factory=MCPAgentClientSession
Expand Down
15 changes: 8 additions & 7 deletions examples/mcp_researcher/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

app = MCPApp(name="mcp_root_test")


async def example_usage():
async with app.run() as agent_app:
folder_path = Path("agent_folder")
Expand All @@ -22,13 +23,13 @@ async def example_usage():

# Overwrite the config because full path to agent folder needs to be passed
context.config.mcp.servers["interpreter"].args = [
"run",
"-i",
"--rm",
"--pull=always",
"-v",
f"{os.path.abspath('agent_folder')}:/mnt/data/",
"ghcr.io/evalstate/mcp-py-repl:latest",
"run",
"-i",
"--rm",
"--pull=always",
"-v",
f"{os.path.abspath('agent_folder')}:/mnt/data/",
"ghcr.io/evalstate/mcp-py-repl:latest",
]

async with MCPConnectionManager(context.server_registry):
Expand Down
185 changes: 185 additions & 0 deletions examples/workflow_mcp_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Workflow MCP Server Example

This example demonstrates three approaches to creating agents and workflows:

1. Traditional workflow-based approach with manual agent creation
2. Programmatic agent configuration using AgentConfig
3. Declarative agent configuration using FastMCPApp decorators

All three approaches can use `app_server.py` to expose the agents and workflows as an MCP server.

## Concepts Demonstrated

- Using the `Workflow` base class to create custom workflows
- Registering workflows with an `MCPApp`
- Creating and registering agent configurations with both programmatic and declarative approaches
- Exposing workflows and agents as MCP tools using `app_server.py`
- Connecting to a workflow server using `gen_client`
- Lazy instantiation of agents from configurations when their tools are called

## Components in this Example

1. **DataProcessorWorkflow**: A traditional workflow that processes data in three steps:

- Finding and retrieving content from a source (file or URL)
- Analyzing the content
- Formatting the results

2. **SummarizationWorkflow**: A traditional workflow that summarizes text content:

- Generates a concise summary
- Extracts key points
- Returns structured data

3. **Research Team**: A parallel workflow created using the agent configuration system:

- Uses a fan-in/fan-out pattern with multiple specialized agents
- Demonstrates declarative workflow pattern configuration

4. **Specialist Router**: A router workflow created using FastMCPApp decorators:
- Routes requests to specialized agents based on content
- Shows how to use the decorator syntax for workflow creation

## How to Run

1. Copy the example secrets file:

```
cp mcp_agent.secrets.yaml.example mcp_agent.secrets.yaml
```

2. Edit `mcp_agent.secrets.yaml` to add your API keys.

3. Run the client, which will automatically start the server:
```
uv run client.py
```

## Code Structure

- `basic_agent_server.py`: Defines the BasicAgentWorkflow and creates an MCP server
- `client.py`: Connects to the server and runs the workflow
- `mcp_agent.config.yaml`: Configuration for MCP servers
- `mcp_agent.secrets.yaml`: Secret API keys (not included in repository)

## Understanding the Code

### Approach 1: Traditional Workflow Definition

Workflows are defined by subclassing the `Workflow` base class and implementing:

- The `run` method containing the main workflow logic
- Optional:`initialize` and `cleanup` methods for setup and teardown
- Optional: a custom `create` class method for specialized instantiation

Workflows are registered with the MCPApp using the `@app.workflow` decorator:

Example:

```python
app = MCPApp(name="workflow_mcp_server")

@app.workflow
class DataProcessorWorkflow(Workflow[str]):
@classmethod
async def create(cls, executor: Executor, name: str | None = None, **kwargs: Any) -> "DataProcessorWorkflow":
# Custom instantiation logic
workflow = cls(executor=executor, name=name, **kwargs)
await workflow.initialize()
return workflow

async def initialize(self):
# Set up resources like agents and LLMs

async def run(self, source: str, analysis_prompt: Optional[str] = None, output_format: Optional[str] = None) -> WorkflowResult[str]:
# Workflow implementation...

async def cleanup(self):
# Clean up resources
```

### Approach 2: Programmatic Agent Configuration

Agent configurations can be created programmatically using Pydantic models:

```python
# Create a basic agent configuration
research_agent_config = AgentConfig(
name="researcher",
instruction="You are a helpful research assistant that finds information and presents it clearly.",
server_names=["fetch", "filesystem"],
llm_config=AugmentedLLMConfig(
factory=OpenAIAugmentedLLM,
)
)

# Create a parallel workflow configuration
research_team_config = AgentConfig(
name="research_team",
instruction="You are a research team that produces high-quality, accurate content.",
parallel_config=ParallelWorkflowConfig(
fan_in_agent="editor",
fan_out_agents=["summarizer", "fact_checker"],
)
)

# Register the configurations with the app
app.register_agent_config(research_agent_config)
app.register_agent_config(research_team_config)
```

### Approach 3: Declarative Agent Configuration with FastMCPApp

FastMCPApp provides decorators for creating agent configurations in a more declarative style:

```python
fast_app = FastMCPApp(name="fast_workflow_mcp_server")

# Basic agent with OpenAI LLM
@fast_app.agent("assistant", "You are a helpful assistant that answers questions concisely.",
server_names=["calculator"])
def assistant_config(config):
config.llm_config = AugmentedLLMConfig(
factory=OpenAIAugmentedLLM,
)
return config

# Router workflow with specialist agents
@fast_app.router("specialist_router", "You route requests to the appropriate specialist.",
agent_names=["mathematician", "programmer", "writer"])
def router_config(config):
config.llm_config = AugmentedLLMConfig(
factory=OpenAIAugmentedLLM
)
config.router_config.top_k = 1
return config
```

### Exposing Workflows and Agents as Tools

The MCP server automatically exposes both workflows and agent configurations as tools:

**Workflow tools**:

- Running a workflow: `workflows/{workflow_id}/run`
- Checking status: `workflows/{workflow_id}/get_status`
- Controlling workflow execution: `workflows/resume`, `workflows/cancel`

**Agent tools**:

- Running an agent: `agents/{agent_name}/generate`
- Getting string response: `agents/{agent_name}/generate_str`
- Getting structured response: `agents/{agent_name}/generate_structured`

Agent configurations are lazily instantiated when their tools are called. If the agent is already active, the existing instance is reused.

### Connecting to the Workflow Server

The client connects to the workflow server using the `gen_client` function:

```python
async with gen_client("workflow_server", context.server_registry) as server:
# Connect and use the server
```

You can then call both workflow and agent tools through this client connection.
132 changes: 132 additions & 0 deletions examples/workflow_mcp_server/basic_agent_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
Workflow MCP Server Example

This example demonstrates three approaches to creating agents and workflows:
1. Traditional workflow-based approach with manual agent creation
2. Programmatic agent configuration using AgentConfig
3. Declarative agent configuration using FastMCPApp decorators
"""

import asyncio
import os
import logging

from mcp_agent.app import MCPApp
from mcp_agent.app_server import create_mcp_server_for_app
from mcp_agent.agents.agent import Agent
from mcp_agent.workflows.llm.augmented_llm import RequestParams
from mcp_agent.workflows.llm.llm_selector import ModelPreferences
from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
from mcp_agent.executor.workflow import Workflow, WorkflowResult

# Initialize logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Create a single FastMCPApp instance (which extends MCPApp)
app = MCPApp(name="basic_agent_server", description="Basic agent server example")


@app.workflow
class BasicAgentWorkflow(Workflow[str]):
"""
A basic workflow that demonstrates how to create a simple agent.
This workflow is used as an example of a basic agent configuration.
"""

async def run(self, input: str) -> WorkflowResult[str]:
"""
Run the basic agent workflow.

Args:
input: The input string to prompt the agent.

Returns:
WorkflowResult containing the processed data.
"""

logger = app.logger
context = app.context

logger.info("Current config:", data=context.config.model_dump())
logger.info("Received input:", data=input)

# Add the current directory to the filesystem server's args
context.config.mcp.servers["filesystem"].args.extend([os.getcwd()])

finder_agent = Agent(
name="finder",
instruction="""You are an agent with access to the filesystem,
as well as the ability to fetch URLs. Your job is to identify
the closest match to a user's request, make the appropriate tool calls,
and return the URI and CONTENTS of the closest match.""",
server_names=["fetch", "filesystem"],
)

async with finder_agent:
logger.info("finder: Connected to server, calling list_tools...")
result = await finder_agent.list_tools()
logger.info("Tools available:", data=result.model_dump())

llm = await finder_agent.attach_llm(OpenAIAugmentedLLM)
result = await llm.generate_str(
message="Print the contents of mcp_agent.config.yaml verbatim",
)
logger.info(f"mcp_agent.config.yaml contents: {result}")

# Let's switch the same agent to a different LLM
llm = await finder_agent.attach_llm(AnthropicAugmentedLLM)

result = await llm.generate_str(
message="Print the first 2 paragraphs of https://modelcontextprotocol.io/introduction",
)
logger.info(f"First 2 paragraphs of Model Context Protocol docs: {result}")

# Multi-turn conversations
result = await llm.generate_str(
message="Summarize those paragraphs in a 128 character tweet",
# You can configure advanced options by setting the request_params object
request_params=RequestParams(
# See https://modelcontextprotocol.io/docs/concepts/sampling#model-preferences for more details
modelPreferences=ModelPreferences(
costPriority=0.1,
speedPriority=0.2,
intelligencePriority=0.7,
),
# You can also set the model directly using the 'model' field
# Generally request_params type aligns with the Sampling API type in MCP
),
)
logger.info(f"Paragraph as a tweet: {result}")
return WorkflowResult(value=result)


async def main():
async with app.run() as agent_app:
# Add the current directory to the filesystem server's args if needed
context = agent_app.context
if "filesystem" in context.config.mcp.servers:
context.config.mcp.servers["filesystem"].args.extend([os.getcwd()])

# Log registered workflows and agent configurations
logger.info(f"Creating MCP server for {agent_app.name}")

logger.info("Registered workflows:")
for workflow_id in agent_app.workflows:
logger.info(f" - {workflow_id}")

logger.info("Registered agent configurations:")
for name, config in agent_app.agent_configs.items():
workflow_type = config.get_agent_type() or "basic"
logger.info(f" - {name} ({workflow_type})")

# Create the MCP server that exposes both workflows and agent configurations
mcp_server = create_mcp_server_for_app(agent_app)

# Run the server
await mcp_server.run_stdio_async()


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading