General OSS health (#643)
This commit is contained in:
parent
49e53d2b33
commit
5f55258268
75 changed files with 90 additions and 329 deletions
121
README.md
121
README.md
|
|
@ -32,16 +32,16 @@
|
|||
</div>
|
||||
|
||||
<p align="center" style="display: flex; justify-content: center; gap: 5px; font-size: 15px;">
|
||||
<a href="https://docs.arcade.dev/home" target="_blank">Documentation</a> •
|
||||
<a href="https://docs.arcade.dev/tools" target="_blank">Tools</a> •
|
||||
<a href="https://docs.arcade.dev/home/quickstart" target="_blank">Quickstart</a> •
|
||||
<a href="https://docs.arcade.dev/tools" target="_blank">Prebuilt Tools</a> •
|
||||
<a href="https://docs.arcade.dev/home/contact-us" target="_blank">Contact Us</a>
|
||||
|
||||
# Arcade MCP Server Framework
|
||||
|
||||
**To learn more about Arcade.dev, check out our [documentation](https://docs.arcade.dev/home).**
|
||||
* **To see example servers built with Arcade MCP Server Framework (this repo), check out our [examples](examples/)**
|
||||
|
||||
**To learn more about the Arcade MCP Server Framework, check out our [Arcade MCP documentation](https://python.mcp.arcade.dev/)**
|
||||
* **To learn more about the Arcade MCP Server Framework (this repo), check out our [Arcade MCP documentation](https://docs.arcade.dev/en/home/build-tools/create-a-mcp-server)**
|
||||
|
||||
* **To learn more about other offerings from Arcade.dev, check out our [documentation](https://docs.arcade.dev/home).**
|
||||
|
||||
_Pst. hey, you, give us a star if you like it!_
|
||||
|
||||
|
|
@ -49,13 +49,13 @@ _Pst. hey, you, give us a star if you like it!_
|
|||
<img src="https://img.shields.io/github/stars/ArcadeAI/arcade-mcp.svg" alt="GitHub stars">
|
||||
</a>
|
||||
|
||||
### Quick Start: Create a New Server
|
||||
## Quick Start: Create a New Server
|
||||
|
||||
The fastest way to get started is with the `arcade new` command, which creates a complete MCP server project:
|
||||
The fastest way to get started is with the `arcade new` CLI command, which creates a complete MCP server project:
|
||||
|
||||
```bash
|
||||
# Install the CLI
|
||||
uv pip install arcade-mcp
|
||||
uv tool install arcade-mcp
|
||||
|
||||
# Create a new server project
|
||||
arcade new my_server
|
||||
|
|
@ -64,7 +64,7 @@ arcade new my_server
|
|||
cd my_server
|
||||
```
|
||||
|
||||
This generates a complete project with:
|
||||
This generates a project with:
|
||||
|
||||
- **server.py** - Main server file with MCPApp and example tools
|
||||
|
||||
|
|
@ -72,24 +72,84 @@ This generates a complete project with:
|
|||
|
||||
- **.env.example** - Example `.env` file containing a secret required by one of the generated tools in `server.py`
|
||||
|
||||
The generated `server.py` includes proper command-line argument handling:
|
||||
The generated `server.py` includes proper command-line argument handling and three example tools:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""simple_server MCP server"""
|
||||
|
||||
import sys
|
||||
from typing import Annotated
|
||||
from arcade_mcp_server import MCPApp
|
||||
|
||||
app = MCPApp(name="my_server", version="1.0.0")
|
||||
import httpx
|
||||
from arcade_mcp_server import Context, MCPApp
|
||||
from arcade_mcp_server.auth import Reddit
|
||||
|
||||
app = MCPApp(name="simple_server", version="1.0.0", log_level="DEBUG")
|
||||
|
||||
|
||||
@app.tool
|
||||
def greet(name: Annotated[str, "Name to greet"]) -> str:
|
||||
"""Greet someone by name."""
|
||||
def greet(name: Annotated[str, "The name of the person to greet"]) -> str:
|
||||
"""Greet a person by name."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
# To use this tool locally, you need to either set the secret in the .env file or as an environment variable
|
||||
@app.tool(requires_secrets=["MY_SECRET_KEY"])
|
||||
def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of the secret"]:
|
||||
"""Reveal the last 4 characters of a secret"""
|
||||
# Secrets are injected into the context at runtime.
|
||||
# LLMs and MCP clients cannot see or access your secrets
|
||||
# You can define secrets in a .env file.
|
||||
try:
|
||||
secret = context.get_secret("MY_SECRET_KEY")
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
return "The last 4 characters of the secret are: " + secret[-4:]
|
||||
|
||||
# To use this tool locally, you need to install the Arcade CLI (uv tool install arcade-mcp)
|
||||
# and then run 'arcade login' to authenticate.
|
||||
@app.tool(requires_auth=Reddit(scopes=["read"]))
|
||||
async def get_posts_in_subreddit(
|
||||
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
|
||||
) -> dict:
|
||||
"""Get posts from a specific subreddit"""
|
||||
# Normalize the subreddit name
|
||||
subreddit = subreddit.lower().replace("r/", "").replace(" ", "")
|
||||
|
||||
# Prepare the httpx request
|
||||
# OAuth token is injected into the context at runtime.
|
||||
# LLMs and MCP clients cannot see or access your OAuth tokens.
|
||||
oauth_token = context.get_auth_token_or_empty()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {oauth_token}",
|
||||
"User-Agent": "{{ toolkit_name }}-mcp-server",
|
||||
}
|
||||
params = {"limit": 5}
|
||||
url = f"https://oauth.reddit.com/r/{subreddit}/hot"
|
||||
|
||||
# Make the request
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the response
|
||||
return response.json()
|
||||
|
||||
# Run with specific transport
|
||||
if __name__ == "__main__":
|
||||
transport = sys.argv[1] if len(sys.argv) > 1 else "http"
|
||||
# Get transport from command line argument, default to "stdio"
|
||||
# - "stdio" (default): Standard I/O for Claude Desktop, CLI tools, etc.
|
||||
# Supports tools that require_auth or require_secrets out-of-the-box
|
||||
# - "http": HTTPS streaming for Cursor, VS Code, etc.
|
||||
# Does not support tools that require_auth or require_secrets unless the server is deployed
|
||||
# using 'arcade deploy' or added in the Arcade Developer Dashboard with 'Arcade' server type
|
||||
transport = sys.argv[1] if len(sys.argv) > 1 else "stdio"
|
||||
|
||||
# Run the server
|
||||
app.run(transport=transport, host="127.0.0.1", port=8000)
|
||||
|
||||
```
|
||||
|
||||
This approach gives you:
|
||||
|
|
@ -106,11 +166,11 @@ This approach gives you:
|
|||
Run your server directly with Python:
|
||||
|
||||
```bash
|
||||
# Run with HTTP transport (default)
|
||||
# Run with stdio transport (default)
|
||||
uv run server.py
|
||||
|
||||
# Run with stdio transport (for Claude Desktop)
|
||||
uv run server.py stdio
|
||||
# Run with http transport via command line argument
|
||||
uv run server.py http
|
||||
|
||||
# Or use python directly
|
||||
python server.py http
|
||||
|
|
@ -124,26 +184,15 @@ Your server will start and listen for connections. With HTTP transport, you can
|
|||
Once your server is running, connect it to your favorite AI assistant:
|
||||
|
||||
```bash
|
||||
# Configure Claude Desktop (configures for stdio)
|
||||
arcade configure claude --from-local
|
||||
|
||||
# Configure Cursor (configures for http streamable)
|
||||
arcade configure cursor --from-local
|
||||
|
||||
# Configure VS Code (configures for http streamable)
|
||||
arcade configure vscode --from-local
|
||||
arcade configure claude # Configure Claude Desktop to connect to your stdio server in your current directory
|
||||
arcade configure cursor --transport http --port 8080 # Configure Cursor to connect to your local HTTP server on port 8080
|
||||
arcade configure vscode --entrypoint my_server.py # Configure VSCode to connect to your stdio server that will run when my_server.py is executed directly
|
||||
```
|
||||
|
||||
## Client Libraries
|
||||
|
||||
- **[ArcadeAI/arcade-py](https://github.com/ArcadeAI/arcade-py):**
|
||||
The Python client for interacting with Arcade.
|
||||
|
||||
- **[ArcadeAI/arcade-js](https://github.com/ArcadeAI/arcade-js):**
|
||||
The JavaScript client for interacting with Arcade.
|
||||
|
||||
- **[ArcadeAI/arcade-go](https://github.com/ArcadeAI/arcade-go):**
|
||||
The Go client for interacting with Arcade.
|
||||
## Installing this Repo from Source
|
||||
```bash
|
||||
git clone https://github.com/ArcadeAI/arcade-mcp.git && cd arcade-mcp && make install
|
||||
```
|
||||
|
||||
## Support and Community
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 651 KiB |
2
examples/.gitignore
vendored
2
examples/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
# Un-ignore poetry.lock because the projects in this directory represent runnable sample apps
|
||||
!poetry.lock
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
"""
|
||||
This example demonstrates how to directly call a tool that does not require authorization.
|
||||
"""
|
||||
|
||||
from arcadepy import Arcade # pip install arcade-py
|
||||
|
||||
|
||||
def call_non_auth_tool(client: Arcade, user_id: str) -> None:
|
||||
"""Directly call a prebuilt tool that does not require authorization.
|
||||
|
||||
In this example, we are
|
||||
1. Preparing the inputs to the Math.Add tool
|
||||
2. Executing the tool
|
||||
3. Printing the output of the tool's execution, i.e., the result of adding 9001 and 42
|
||||
|
||||
This is a simple example of calling a non-auth tool. Next, try writing your own non-auth tool for your own use case.
|
||||
"""
|
||||
# Prepare the inputs to the tool as a dictionary where keys are the names of the parameters expected by the tool and the values are the actual values to pass to the tool
|
||||
tool_input = {"a": 9001, "b": 42}
|
||||
|
||||
# Execute the tool
|
||||
response = client.tools.execute(
|
||||
tool_name="Math.Add",
|
||||
input=tool_input,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Print the output of the tool execution
|
||||
print(response.output.value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cloud_host = "https://api.arcade.dev"
|
||||
|
||||
client = Arcade(
|
||||
base_url=cloud_host, # Alternatively, use http://localhost:9099 if you are running Arcade Engine locally, or any base_url if you're hosting elsewhere
|
||||
)
|
||||
|
||||
user_id = "you@example.com"
|
||||
call_non_auth_tool(client, user_id)
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
"""
|
||||
This example demonstrates how to directly call a tool that requires authorization.
|
||||
"""
|
||||
|
||||
from arcadepy import Arcade # pip install arcade-py
|
||||
|
||||
|
||||
def call_auth_tool(client: Arcade, user_id: str) -> None:
|
||||
"""Directly call a prebuilt tool that requires authorization.
|
||||
|
||||
In this example, we are
|
||||
1. Authorizing Arcade to read emails from the user's Gmail account with the user's permission to do so
|
||||
2. Reading 5 emails from the user's Gmail account
|
||||
3. Printing the emails
|
||||
|
||||
Try altering this example to call a tool that requires a different authorization.
|
||||
"""
|
||||
# Start the authorization process
|
||||
auth_response = client.tools.authorize(
|
||||
tool_name="Gmail.ListEmails",
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# If not already authorized, then wait for the user to authorize the permissions required by the tool
|
||||
if auth_response.status != "completed":
|
||||
print(f"Click this link to authorize: {auth_response.url}")
|
||||
|
||||
# Wait for the user to complete the auth flow, if necessary
|
||||
client.auth.wait_for_completion(auth_response)
|
||||
|
||||
# Prepare the inputs to the tool as a dictionary where keys are the names of the parameters expected by the tool and the values are the actual values to pass to the tool
|
||||
tool_input = {"n_emails": 5}
|
||||
|
||||
# Execute the tool
|
||||
response = client.tools.execute(
|
||||
tool_name="Gmail.ListEmails",
|
||||
input=tool_input,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Print the output of the tool execution.
|
||||
print(response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = Arcade(
|
||||
base_url="https://api.arcade.dev", # Alternatively, use http://localhost:9099 if you are running Arcade Engine locally, or any base_url if you're hosting elsewhere
|
||||
)
|
||||
|
||||
user_id = "you@example.com"
|
||||
call_auth_tool(client, user_id)
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""
|
||||
This example shows how to call a tool that requires authorization with an LLM using the OpenAI Python client.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
def call_tool_with_openai(client: OpenAI) -> dict:
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "user", "content": "Star the ArcadeAI/arcade-mcp repository."},
|
||||
],
|
||||
model="gpt-4o-mini", # TODO: Try "claude-3-5-sonnet-20240620" or other models from our supported model providers. Checkout out our docs for a full list https://docs.arcade.dev
|
||||
user="you@example.com",
|
||||
tools=["Github.SetStarred"],
|
||||
tool_choice="generate", # TODO: Try "execute" and note any differences
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arcade_api_key = os.environ.get(
|
||||
"ARCADE_API_KEY"
|
||||
) # If you forget your Arcade API key, it is stored at ~/.arcade/credentials.yaml on `arcade login`
|
||||
cloud_host = "https://api.arcade.dev" + "/v1"
|
||||
|
||||
openai_client = OpenAI(
|
||||
api_key=arcade_api_key,
|
||||
base_url=cloud_host, # Alternatively, use http://localhost:9099/v1 if you are running Arcade Engine locally
|
||||
)
|
||||
|
||||
chat_result = call_tool_with_openai(openai_client)
|
||||
# If the tool call requires authorization, then wait for the user to authorize and then call the tool again
|
||||
if (
|
||||
chat_result.choices[0].tool_authorizations
|
||||
and chat_result.choices[0].tool_authorizations[0].get("status") == "pending"
|
||||
):
|
||||
print(chat_result.choices[0].message.content)
|
||||
input("After you have authorized, press Enter to continue...")
|
||||
chat_result = call_tool_with_openai(openai_client)
|
||||
|
||||
print(chat_result.choices[0].message.content)
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
"""
|
||||
This example demonstrates how to get an authorization token for a user and then use it to make a request to the Google API on behalf of the user.
|
||||
"""
|
||||
|
||||
from arcadepy import Arcade
|
||||
from google.oauth2.credentials import Credentials # pip install google-auth
|
||||
from googleapiclient.discovery import build # pip install google-api-python-client
|
||||
|
||||
|
||||
def get_auth_token(client: Arcade, user_id: str) -> str:
|
||||
"""Get an authorization token for a user.
|
||||
|
||||
In this example, we are
|
||||
1. Starting the authorization process for the Gmail Readonly scope
|
||||
2. Waiting for the user to authorize the scope
|
||||
3. Getting the authorization token
|
||||
4. Using the authorization token to make a request to the Google API on behalf of the user
|
||||
"""
|
||||
# Start the authorization process
|
||||
auth_response = client.auth.start(
|
||||
user_id, "google", scopes=["https://www.googleapis.com/auth/gmail.readonly"]
|
||||
)
|
||||
|
||||
if auth_response.status != "completed":
|
||||
print(f"Click this link to authorize: {auth_response.url}")
|
||||
auth_response = client.auth.wait_for_completion(auth_response)
|
||||
|
||||
return auth_response.context.token
|
||||
|
||||
|
||||
def use_auth_token(token: str) -> None:
|
||||
"""Use an authorization token to make a request to the Google API on behalf of a user.
|
||||
|
||||
In this example, we are
|
||||
1. Using the authorization token that we got from the authorization process to make a request to the Google API
|
||||
client.auth.wait_for_completion(auth_response)
|
||||
"""
|
||||
# Use the token from the authorization response
|
||||
creds = Credentials(token)
|
||||
service = build("gmail", "v1", credentials=creds)
|
||||
|
||||
# Now you can use the Google API
|
||||
results = service.users().labels().list(userId="me").execute()
|
||||
labels = results.get("labels", [])
|
||||
print("Labels:", labels)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cloud_host = "https://api.arcade.dev"
|
||||
|
||||
client = Arcade(
|
||||
base_url=cloud_host, # Alternatively, use http://localhost:9099 if you are running Arcade locally, or any base_url if you're hosting elsewhere
|
||||
)
|
||||
|
||||
user_id = "you@example.com"
|
||||
|
||||
token = get_auth_token(client, user_id)
|
||||
use_auth_token(token)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
ARG VERSION=latest
|
||||
|
||||
# Base worker image
|
||||
FROM ghcr.io/arcadeai/worker-base:${VERSION}
|
||||
|
||||
# Copy requirements and constraints
|
||||
COPY toolkits.txt ./
|
||||
|
||||
# Install toolkits from file
|
||||
RUN pip install -r toolkits.txt
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
## Custom Worker Image
|
||||
|
||||
This example shows how to build a custom worker image with toolkits.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
docker build -t custom-worker:0.1.0 .
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```
|
||||
docker run -p 8002:8002 custom-worker:0.1.0
|
||||
```
|
||||
|
||||
### Change the Toolkits
|
||||
|
||||
To change the toolkits, edit the `toolkits.txt` file.
|
||||
|
||||
```
|
||||
arcade-gmail==0.1.0
|
||||
arcade-firecrawl==0.1.0
|
||||
arcade-zoom==0.1.2
|
||||
...
|
||||
```
|
||||
|
|
@ -1 +0,0 @@
|
|||
arcade-gmail
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
## Deploy a Custom Arcade Worker on Modal
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- Modal CLI
|
||||
|
||||
### Deploy
|
||||
|
||||
```bash
|
||||
cd examples/serving-tools
|
||||
modal deploy run-arcade-worker.py
|
||||
```
|
||||
|
||||
### Changing the Toolkits
|
||||
|
||||
To change the toolkits, edit the `toolkits` list in the `run-arcade-worker.py` file.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import os
|
||||
|
||||
from modal import App, Image, asgi_app
|
||||
|
||||
# Define the FastAPI app
|
||||
app = App("arcade-worker")
|
||||
|
||||
toolkits = ["arcade_gmail", "arcade_slack"]
|
||||
|
||||
image = (
|
||||
Image.debian_slim().pip_install("arcade_tdk").pip_install("arcade_serve").pip_install(toolkits)
|
||||
)
|
||||
|
||||
|
||||
@app.function(image=image)
|
||||
@asgi_app()
|
||||
def fastapi_app():
|
||||
from arcade_serve.fastapi.worker import FastAPIWorker
|
||||
from arcade_tdk import Toolkit
|
||||
from fastapi import FastAPI
|
||||
|
||||
web_app = FastAPI()
|
||||
|
||||
# Initialize app and Arcade FastAPIWorker
|
||||
worker_secret = os.environ.get("ARCADE_WORKER_SECRET", "dev")
|
||||
worker = FastAPIWorker(web_app, secret=worker_secret)
|
||||
|
||||
# Register toolkits we've installed
|
||||
installed_toolkits = Toolkit.find_all_arcade_toolkits()
|
||||
for toolkit in installed_toolkits:
|
||||
if toolkit.package_name in toolkits:
|
||||
worker.register_toolkit(toolkit)
|
||||
|
||||
return web_app
|
||||
|
|
@ -14,7 +14,7 @@ Arcade CLI provides a comprehensive command-line interface for the Arcade platfo
|
|||
|
||||
## Installation
|
||||
```bash
|
||||
pip install arcade-mcp
|
||||
uv tool install arcade-mcp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ def configure(
|
|||
Examples:
|
||||
arcade configure claude
|
||||
arcade configure cursor --transport http --port 8080
|
||||
arcade configure vscode --host arcade --entrypoint ../../../mcp/server.py --config .vscode/mcp.json
|
||||
arcade configure vscode --host arcade --entrypoint my_server.py --config .vscode/mcp.json
|
||||
arcade configure claude --host local --name my_server_name
|
||||
"""
|
||||
from arcade_cli.configure import configure_client
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def greet(name: Annotated[str, "The name of the person to greet"]) -> str:
|
|||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
# To use this tool, you need to either set the secret in the .env file or as an environment variable
|
||||
# To use this tool locally, you need to either set the secret in the .env file or as an environment variable
|
||||
@app.tool(requires_secrets=["MY_SECRET_KEY"])
|
||||
def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of the secret"]:
|
||||
"""Reveal the last 4 characters of a secret"""
|
||||
|
|
@ -31,8 +31,8 @@ def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of
|
|||
|
||||
return "The last 4 characters of the secret are: " + secret[-4:]
|
||||
|
||||
# To use this tool, you need to either set your ARCADE_API_KEY as an environment variable or
|
||||
# use the Arcade CLI (uv pip install arcade-mcp) and run 'arcade login' to authenticate.
|
||||
# To use this tool locally, you need to install the Arcade CLI (uv tool install arcade-mcp)
|
||||
# and then run 'arcade login' to authenticate.
|
||||
@app.tool(requires_auth=Reddit(scopes=["read"]))
|
||||
async def get_posts_in_subreddit(
|
||||
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue