Skip to content

Commit

Permalink
Merge pull request #313 from DefangLabs/mcp-with-ui-proxy
Browse files Browse the repository at this point in the history
MCP Sample
  • Loading branch information
commit111 authored Feb 12, 2025
2 parents 23bd272 + ceb4480 commit 9369cfa
Show file tree
Hide file tree
Showing 29 changed files with 8,979 additions and 0 deletions.
1 change: 1 addition & 0 deletions samples/mcp/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm
11 changes: 11 additions & 0 deletions samples/mcp/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/defanglabs/devcontainer-feature/defang-cli:1.0.4": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/aws-cli:1": {}
}
}
25 changes: 25 additions & 0 deletions samples/mcp/.github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Deploy

on:
push:
branches:
- main

jobs:
deploy:
environment: playground
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout Repo
uses: actions/checkout@v4

- name: Deploy
uses: DefangLabs/[email protected]
with:
config-env-vars: ANTHROPIC_API_KEY
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
1 change: 1 addition & 0 deletions samples/mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
70 changes: 70 additions & 0 deletions samples/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# MCP

[![1-click-deploy](https://defang.io/deploy-with-defang.png)](https://portal.defang.dev/redirect?url=https%3A%2F%2Fgithub.com%2Fnew%3Ftemplate_name%3Dsample-mcp-template%26template_owner%3DDefangSamples)

This is a sample of an MCP (Model Context Protocol) chatbot application built with Next.js, Python, and Anthropic Claude, deployed using Defang.

This example uses Docker's [`mcp/time`](https://hub.docker.com/r/mcp/time) image as a base for the MCP server (with MCP tools included), but it can be adapted to use any of the Docker [MCP images](https://hub.docker.com/u/mcp).

### How It Works

The [MCP client](https://modelcontextprotocol.io/quickstart/client) is written in Python and ran in a `venv` virtual environment. The MCP server is provided by the Docker `mcp/time` image. The MCP server communicates with the MCP client in a Quart app (i.e. Asynchronous Server Gateway Interface (ASGI) version of Flask) through the `stdio` transport method, as seen in `/mcp-server/main.py`. For more on MCP transport methods, see [here](https://modelcontextprotocol.io/docs/concepts/transports).

The UI (User Interface) and web server are built in Next.js (see `/ui/src/app`). The web server includes a forwarding action to connect to the MCP client.

Here's a breakdown of what happens when you interact with the UI:
1. When a user submits a query to the chatbot, the browser sends a request to the Next.js web server.
2. The Next.js web server will forward this request to the MCP client via a REST endpoint.
3. The MCP client processes the request by interacting with the Anthropic (Claude) API and tools available through the MCP server.
4. Once the response is generated, it is sent back to the Next.js web server and displayed to the user in the UI.

## Prerequisites

1. Download [Defang CLI](https://github.com/DefangLabs/defang)
2. (Optional) If you are using [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) authenticate with your cloud provider account
3. (Optional for local development) [Docker CLI](https://docs.docker.com/engine/install/)

## Development

To run the application locally, you can use the following command:

```bash
docker compose up --build
```

## Configuration
For this sample, you will need to provide the following [configuration](https://docs.defang.io/docs/concepts/configuration):

> Note that if you are using the 1-click deploy option, you can set these values as secrets in your GitHub repository and the action will automatically deploy them for you.
### `ANTHROPIC_API_KEY`
An API key for accessing the [Anthropic Claude API](https://docs.anthropic.com/en/api/getting-started).
```bash
defang config set ANTHROPIC_API_KEY
```

## Deployment

> [!NOTE]
> Download [Defang CLI](https://github.com/DefangLabs/defang)
### Defang Playground

Deploy your application to the Defang Playground by opening up your terminal and typing:
```bash
defang compose up
```

### BYOC

If you want to deploy to your own cloud account, you can [use Defang BYOC](https://docs.defang.io/docs/tutorials/deploy-to-your-cloud).

---

Title: MCP

Short Description: An MCP (Model Context Protocol) chatbot assistant built with Next.js, Python, and Anthropic Claude.

Tags: MCP, Next.js, Python, Quart, Claude, AI, Anthropic, TypeScript, React, JavaScript

Languages: nodejs
30 changes: 30 additions & 0 deletions samples/mcp/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
services:
ui:
# uncomment to add your own domain
# domainname: example.com
build:
context: ./ui
dockerfile: Dockerfile
ports:
- target: 3000
published: 3000
mode: ingress
deploy:
resources:
reservations:
memory: 256M
environment:
- MCP_SERVER_URL=http://mcp-server:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]

mcp-server:
build:
context: ./mcp-server
dockerfile: Dockerfile
ports:
- target: 8000
published: 8000
mode: host
environment:
- ANTHROPIC_API_KEY
2 changes: 2 additions & 0 deletions samples/mcp/mcp-server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
venv
.env
2 changes: 2 additions & 0 deletions samples/mcp/mcp-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
venv
1 change: 1 addition & 0 deletions samples/mcp/mcp-server/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
18 changes: 18 additions & 0 deletions samples/mcp/mcp-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM mcp/time:latest

WORKDIR /wrapper

COPY requirements.txt ./requirements.txt

# create a virtual environment
RUN python3 -m venv venv && \
# activate the virtual environment
. venv/bin/activate && \
pip3 install --upgrade pip && \
pip3 install -r requirements.txt && \
# deactivate the virtual environment
deactivate

COPY . .

ENTRYPOINT ["./run.sh"]
161 changes: 161 additions & 0 deletions samples/mcp/mcp-server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import asyncio
import os
from typing import Optional
from contextlib import AsyncExitStack

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from anthropic import Anthropic
import logging
from quart import Quart, request, jsonify
from quart_cors import cors

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

# Define an MCP client
class MCPClient:
def __init__(self):
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.tools = []


async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server
Args:
server_script_path: Path to the server script
"""

# run the command to start the server
server_params = StdioServerParameters(
command="/app/.venv/bin/python",
args=[server_script_path, "--local-timezone=America/Los_Angeles"],
env=None
)
try:
logger.info("Starting async context")
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

await self.session.initialize()

# List available tools
response = await self.session.list_tools()
self.tools = response.tools

logger.info("\nConnected to server with tools:", [tool.name for tool in self.tools])

except Exception as e:
logger.error(f"Failed to connect to server: {e}")
await self.cleanup()
raise

async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]

available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in self.tools]

# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)

# Process response and handle tool calls
tool_results = []
final_text = []

# loops through content, and if content.type is a tool_use, calls the tool
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
logger.info(f"Noticed tool {tool_name} with args {tool_args}")

if self.session is None:
logger.error("Session not initialized. Exiting.")
return jsonify({"response": "Session not initialized. Exiting."})

try:
# Execute tool call
await self.session.initialize()
result = await self.session.call_tool(tool_name, tool_args)

tool_results.append({"call": tool_name, "result": result})
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")

# Continue conversation with tool results
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
except Exception as e:
logger.error(f"Failed to call tool {tool_name}: {(e)}")
final_text.append(f"Failed to call tool {tool_name}: {(e)}")
return jsonify({"response": "\n".join(final_text)})

# Get completed response from Claude with tool results
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)

final_text.append(response.content[0].text)

logger.info(f"Final text: {final_text}")
logger.info(f"Tool Results: {tool_results}")
return jsonify({"response": response.content[0].text})

async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()

# let's start a Quart server
app = Quart(__name__)
app = cors(app, allow_origin="*")

@app.route('/', methods=['POST'])
async def chat():
client = MCPClient()
try:
data = await request.get_json()
query = data.get('messages', [None])[0]
logger.info(f"Received query: {query}")
await client.connect_to_server("/app/.venv/bin/mcp-server-time")
return await client.process_query(query)
finally:
await client.cleanup()

async def main():
print("app OK")

if __name__ == "__main__":
app.run(port=8000, host='0.0.0.0')
asyncio.run(main())
7 changes: 7 additions & 0 deletions samples/mcp/mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "mcp-client"
version = "0.1.0"
description = "MCP client to query Anthropic's Claude AI and utilize MCP tools to augment response."
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
28 changes: 28 additions & 0 deletions samples/mcp/mcp-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
annotated-types==0.7.0
anthropic==0.45.2
anyio==4.8.0
certifi==2025.1.31
click==8.1.8
distro==1.9.0
exceptiongroup==1.2.2
httpcore==1.0.7
httpx==0.28.1
httpx-sse==0.4.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.5
jiter==0.8.2
MarkupSafe==3.0.2
mcp==1.2.1
pydantic==2.10.6
pydantic-settings==2.7.1
pydantic_core==2.27.2
python-dotenv==1.0.1
quart==0.18.3
quart-cors==0.6.0
sniffio==1.3.1
sse-starlette==2.2.1
starlette==0.45.3
typing_extensions==4.12.2
uvicorn==0.34.0
Werkzeug==2.3.7
10 changes: 10 additions & 0 deletions samples/mcp/mcp-server/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

echo "Starting MCP Server"

# activate the venv
source /wrapper/venv/bin/activate

# run python main.py using the venv
python3 main.py

3 changes: 3 additions & 0 deletions samples/mcp/ui/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.next
.env
3 changes: 3 additions & 0 deletions samples/mcp/ui/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
Loading

0 comments on commit 9369cfa

Please sign in to comment.