Skip to content
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

MCP Sample #313

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1e30ea0
mcp draft
commit111 Jan 24, 2025
c82e5ac
make anthropic messages show up in ui
commit111 Jan 25, 2025
5ef8a56
update workflow and package json
commit111 Jan 27, 2025
a8e8922
update compose file
commit111 Jan 27, 2025
7bdcc0a
draft
commit111 Feb 3, 2025
3cb1934
progress???
raphaeltm Feb 3, 2025
0c16441
refining the mcp python client
commit111 Feb 4, 2025
6be31e7
fix to load modules into virtual env
nullfunc Feb 4, 2025
fea1130
executable mcp
nullfunc Feb 4, 2025
aa95f72
use flask async
nullfunc Feb 4, 2025
7e11134
fix anthropic variable
commit111 Feb 5, 2025
aea7f41
ignore venv from copy. will be generate on docker build
nullfunc Feb 6, 2025
cb1ece6
able to call_tool, but not in an app context
commit111 Feb 7, 2025
2b1e947
add two query-case
commit111 Feb 7, 2025
c4e0a3b
mcp server talks to client in a quart app
commit111 Feb 7, 2025
db03e28
update UI to show claude mcp response
commit111 Feb 7, 2025
93399a7
make chatbot receive query
commit111 Feb 7, 2025
0a295b4
initial message adjustment
commit111 Feb 7, 2025
d022735
update .ignore files
commit111 Feb 7, 2025
74ed9dd
update readme desc
commit111 Feb 7, 2025
29ae8c1
ports update (draft)
commit111 Feb 8, 2025
65caa9f
working but needs clean up
nullfunc Feb 8, 2025
963b65a
move proxy code to server action
raphaeltm Feb 10, 2025
8ea1981
fix file reference
nullfunc Feb 11, 2025
2069efe
update action import
raphaeltm Feb 11, 2025
962f3ed
Merge branch 'mcp-with-ui-proxy' of github.com:DefangLabs/samples int…
raphaeltm Feb 11, 2025
4906802
remove double use of response
commit111 Feb 11, 2025
526c9a6
restructure message requests and responses
raphaeltm Feb 11, 2025
7b3e492
edit readme
commit111 Feb 12, 2025
9f7dff0
remove excess import + comment
commit111 Feb 12, 2025
ad2a6a9
update starter sample
commit111 Feb 12, 2025
84b1c92
update one-click deploy link
commit111 Feb 12, 2025
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 .github/workflows/deploy-changed-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
if: env.should_continue == 'true'
env:
FIXED_VERIFIER_PK: ${{ secrets.FIXED_VERIFIER_PK }}
TEST_ANTHROPIC_API_KEY: ${{ secrets.TEST_ANTHROPIC_API_KEY }}
TEST_AWS_ACCESS_KEY: ${{ secrets.TEST_AWS_ACCESS_KEY }}
TEST_AWS_SECRET_KEY: ${{ secrets.TEST_AWS_SECRET_KEY }}
TEST_BOARD_PASSWORD: ${{ secrets.TEST_BOARD_PASSWORD }}
Expand Down
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
67 changes: 67 additions & 0 deletions samples/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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 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 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`. 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. 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).

1. When a user submits a query to the chatbot, the browser sends a request to the Next.js UI.
2. The UI will forward this request to the MCP client via a REST endpoint.
3. The MCP client processes the request by interacting with Anthropic (Claude) API, and tools available through the MCP server.
4. Once the response is generated, it is sent back to the UI and displayed to the user.

## 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 Anthropic AI.
```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 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.9
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"]
Empty file.
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 = []

# methods will go here

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

Args:
server_script_path: Path to the server script (.py or .js)
"""

# 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

print("\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 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 = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
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