diff --git a/packages/mcp-fastmcp/README.md b/packages/mcp-fastmcp/README.md index 44d4cc2..83ece53 100644 --- a/packages/mcp-fastmcp/README.md +++ b/packages/mcp-fastmcp/README.md @@ -714,6 +714,16 @@ This is because FastMCP automatically appends the `/mcp` path to your base URL f ## Examples +### Hello World Server + +A minimal example demonstrating FastMCP integration with Keycard authentication is available at [`examples/hello_world_server/`](examples/hello_world_server/). + +```bash +cd examples/hello_world_server +export KEYCARD_ZONE_ID="your-zone-id" +uv sync && uv run python main.py +``` + For complete examples and advanced usage patterns, see our [documentation](https://docs.keycard.ai). ## License diff --git a/packages/mcp-fastmcp/examples/hello_world_server/.python-version b/packages/mcp-fastmcp/examples/hello_world_server/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/packages/mcp-fastmcp/examples/hello_world_server/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/packages/mcp-fastmcp/examples/hello_world_server/README.md b/packages/mcp-fastmcp/examples/hello_world_server/README.md new file mode 100644 index 0000000..a81de94 --- /dev/null +++ b/packages/mcp-fastmcp/examples/hello_world_server/README.md @@ -0,0 +1,84 @@ +# Hello World MCP Server with Keycard Authentication + +A minimal example demonstrating how to add Keycard authentication to a FastMCP server. + +## Why Keycard? + +Keycard lets you securely connect your AI IDE or agent to external resources. It provides OAuth-based authentication for your MCP server plus auditability—so you know who accessed what. + +## Prerequisites + +Before running this example, set up Keycard: + +1. **Sign up** at [keycard.ai](https://keycard.ai) +2. **Create a zone** — this is your authentication boundary +3. **Configure an identity provider** (Google, Microsoft, etc.) — this is how your users will sign in +4. **Create an MCP resource** with URL `http://localhost:8000/` — this registers your server with Keycard + +Once configured, get your **zone ID** from the Keycard console. See [MCP Server Setup](https://docs.keycard.ai/build-with-keycard/mcp-server) for detailed instructions. + +## Quick Start + +### 1. Set Environment Variables + +```bash +export KEYCARD_ZONE_ID="your-zone-id" +export MCP_SERVER_URL="http://localhost:8000/" +``` + +### 2. Install Dependencies + +```bash +cd packages/mcp-fastmcp/examples/hello_world_server +uv sync +``` + +### 3. Run the Server + +```bash +uv run python main.py +``` + +The server will start on `http://localhost:8000`. + +### 4. Verify the Server + +Check that OAuth metadata is being served: + +```bash +curl http://localhost:8000/.well-known/oauth-authorization-server +``` + +You should see JSON with `issuer`, `authorization_endpoint`, and other OAuth metadata. + +## Testing with MCP Client + +Connect to your server using any MCP-compatible client (e.g., Cursor, Claude Desktop) and authenticate through your configured identity provider. + +## Adding Delegated Access + +To enable the `@grant` decorator for accessing external APIs on behalf of users: + +1. Get client credentials from your Keycard zone +2. Set additional environment variables: + +```bash +export KEYCARD_CLIENT_ID="your-client-id" +export KEYCARD_CLIENT_SECRET="your-client-secret" +``` + +3. Uncomment the `get_external_data` tool in `main.py` + +## Environment Variables Reference + +| Variable | Required | Description | +|----------|----------|-------------| +| `KEYCARD_ZONE_ID` | Yes | Your Keycard zone ID | +| `MCP_SERVER_URL` | No | Server URL (default: `http://localhost:8000/`) | +| `KEYCARD_CLIENT_ID` | No | Client ID for delegated access | +| `KEYCARD_CLIENT_SECRET` | No | Client secret for delegated access | + +## Learn More + +- [Keycard Documentation](https://docs.keycard.ai) +- [FastMCP Documentation](https://docs.fastmcp.com) diff --git a/packages/mcp-fastmcp/examples/hello_world_server/main.py b/packages/mcp-fastmcp/examples/hello_world_server/main.py new file mode 100644 index 0000000..e9e612e --- /dev/null +++ b/packages/mcp-fastmcp/examples/hello_world_server/main.py @@ -0,0 +1,74 @@ +"""Hello World MCP Server with Keycard Authentication. + +A minimal example demonstrating FastMCP integration with Keycard OAuth. +""" + +import os + +from fastmcp import FastMCP + +from keycardai.mcp.integrations.fastmcp import AuthProvider + +# Configure Keycard authentication +# Get your zone_id from console.keycard.ai +auth_provider = AuthProvider( + zone_id=os.getenv("KEYCARD_ZONE_ID", "your-zone-id"), + mcp_server_name="Hello World Server", + mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000/"), +) + +# Get the RemoteAuthProvider for FastMCP +auth = auth_provider.get_remote_auth_provider() + +# Create authenticated FastMCP server +mcp = FastMCP("Hello World Server", auth=auth) + + +@mcp.tool() +def hello_world(name: str) -> str: + """Say hello to an authenticated user. + + Args: + name: The name to greet + + Returns: + A personalized greeting message + """ + return f"Hello, {name}! You are authenticated." + + +# Example tool with delegated access (uncomment to use) +# Requires KEYCARD_CLIENT_ID and KEYCARD_CLIENT_SECRET env vars +# +# from fastmcp import Context +# from keycardai.mcp.integrations.fastmcp import AccessContext +# +# @mcp.tool() +# @auth_provider.grant("https://api.example.com") +# def get_external_data(ctx: Context, query: str) -> str: +# """Fetch data from an external API using delegated access. +# +# Args: +# ctx: FastMCP context with authentication state +# query: Search query for the external API +# +# Returns: +# Data from the external API or error message +# """ +# access_context: AccessContext = ctx.get_state("keycardai") +# +# if access_context.has_errors(): +# return f"Token exchange failed: {access_context.get_errors()}" +# +# token = access_context.access("https://api.example.com").access_token +# # Use token to call external API +# return f"Fetched data for query: {query}" + + +def main(): + """Entry point for the MCP server.""" + mcp.run(transport="streamable-http") + + +if __name__ == "__main__": + main() diff --git a/packages/mcp-fastmcp/examples/hello_world_server/pyproject.toml b/packages/mcp-fastmcp/examples/hello_world_server/pyproject.toml new file mode 100644 index 0000000..ee948dc --- /dev/null +++ b/packages/mcp-fastmcp/examples/hello_world_server/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "hello-world-server" +version = "0.1.0" +description = "A minimal FastMCP server with Keycard authentication" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "keycardai-mcp-fastmcp", + "fastmcp>=2.13.0,<3.0.0", +] + +[tool.uv.sources] +keycardai-mcp-fastmcp = { path = "../../", editable = true } + +[project.scripts] +hello-world-server = "main:main" diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 9dff915..f884bcb 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -682,6 +682,18 @@ middleware = [ ## Examples +### Hello World Server + +A minimal example demonstrating low-level MCP integration with Keycard authentication is available at [`examples/hello_world_server/`](examples/hello_world_server/). + +```bash +cd examples/hello_world_server +export KEYCARD_ZONE_ID="your-zone-id" +uv sync && uv run python main.py +``` + +> **Note**: For most use cases, we recommend using the [FastMCP integration](https://pypi.org/project/keycardai-mcp-fastmcp/) which provides a simpler API. This low-level approach is for advanced scenarios requiring more control. + For complete examples and advanced usage patterns, see our [documentation](https://docs.keycard.ai). ## License diff --git a/packages/mcp/examples/hello_world_server/.python-version b/packages/mcp/examples/hello_world_server/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/packages/mcp/examples/hello_world_server/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/packages/mcp/examples/hello_world_server/README.md b/packages/mcp/examples/hello_world_server/README.md new file mode 100644 index 0000000..966a249 --- /dev/null +++ b/packages/mcp/examples/hello_world_server/README.md @@ -0,0 +1,97 @@ +# Hello World MCP Server (Low-Level Integration) + +A minimal example using the `keycardai-mcp` package's AuthProvider directly. + +**Note**: For most use cases, we recommend using the FastMCP integration (`keycardai-mcp-fastmcp`). This low-level approach is for advanced scenarios requiring more control over the Starlette application. + +## Why Keycard? + +Keycard lets you securely connect your AI IDE or agent to external resources. It provides OAuth-based authentication for your MCP server plus auditability—so you know who accessed what. + +## Prerequisites + +Before running this example, set up Keycard: + +1. **Sign up** at [keycard.ai](https://keycard.ai) +2. **Create a zone** — this is your authentication boundary +3. **Configure an identity provider** (Google, Microsoft, etc.) — this is how your users will sign in +4. **Create an MCP resource** with URL `http://localhost:8000/` — this registers your server with Keycard + +Once configured, get your **zone ID** from the Keycard console. See [MCP Server Setup](https://docs.keycard.ai/build-with-keycard/mcp-server) for detailed instructions. + +## When to Use This + +- Custom middleware requirements +- Non-standard routing needs +- Integration with existing Starlette applications +- Multi-zone authentication scenarios + +## Quick Start + +### 1. Set Environment Variables + +```bash +export KEYCARD_ZONE_ID="your-zone-id" +export MCP_SERVER_URL="http://localhost:8000/" +``` + +### 2. Install Dependencies + +```bash +cd packages/mcp/examples/hello_world_server +uv sync +``` + +### 3. Run the Server + +```bash +uv run python main.py +``` + +The server will start on `http://localhost:8000`. + +### 4. Verify the Server + +Check that OAuth metadata is being served: + +```bash +curl http://localhost:8000/.well-known/oauth-authorization-server +``` + +You should see JSON with `issuer`, `authorization_endpoint`, and other OAuth metadata. + +## Key Differences from FastMCP Integration + +| Feature | FastMCP Integration | Low-Level MCP | +|---------|---------------------|---------------| +| Auth provider | `get_remote_auth_provider()` | `auth_provider.app(mcp)` | +| AccessContext | `ctx.get_state("keycardai")` | Function parameter | +| Server startup | `mcp.run()` | `uvicorn.run(app)` | + +## Adding Delegated Access + +To enable the `@grant` decorator for accessing external APIs on behalf of users: + +1. Get client credentials from your Keycard zone +2. Set additional environment variables: + +```bash +export KEYCARD_CLIENT_ID="your-client-id" +export KEYCARD_CLIENT_SECRET="your-client-secret" +``` + +3. Uncomment the `get_external_data` tool in `main.py` + +## Environment Variables Reference + +| Variable | Required | Description | +|----------|----------|-------------| +| `KEYCARD_ZONE_ID` | Yes | Your Keycard zone ID | +| `MCP_SERVER_URL` | No | Server URL (default: `http://localhost:8000/`) | +| `KEYCARD_CLIENT_ID` | No | Client ID for delegated access | +| `KEYCARD_CLIENT_SECRET` | No | Client secret for delegated access | + +## Learn More + +- [FastMCP Integration Example](../../../mcp-fastmcp/examples/hello_world_server/) +- [Keycard Documentation](https://docs.keycard.ai) diff --git a/packages/mcp/examples/hello_world_server/main.py b/packages/mcp/examples/hello_world_server/main.py new file mode 100644 index 0000000..1ba0846 --- /dev/null +++ b/packages/mcp/examples/hello_world_server/main.py @@ -0,0 +1,75 @@ +"""Hello World MCP Server with Low-Level Keycard Authentication. + +A minimal example demonstrating the keycardai-mcp package's AuthProvider +for scenarios requiring more control than the FastMCP integration. +""" + +import os + +import uvicorn +from mcp.server.fastmcp import FastMCP + +from keycardai.mcp.server.auth import AuthProvider + +# Configure Keycard authentication +# Get your zone_id from console.keycard.ai +auth_provider = AuthProvider( + zone_id=os.getenv("KEYCARD_ZONE_ID", "your-zone-id"), + mcp_server_name="Hello World Server", + mcp_server_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000/"), +) + +# Create MCP server (not authenticated yet) +mcp = FastMCP("Hello World Server") + + +@mcp.tool() +def hello_world(name: str) -> str: + """Say hello to an authenticated user. + + Args: + name: The name to greet + + Returns: + A personalized greeting message + """ + return f"Hello, {name}! You are authenticated." + + +# Example tool with delegated access (uncomment to use) +# Requires KEYCARD_CLIENT_ID and KEYCARD_CLIENT_SECRET env vars +# +# from keycardai.mcp.server.auth import AccessContext +# +# @mcp.tool() +# @auth_provider.grant("https://api.example.com") +# def get_external_data(access_ctx: AccessContext, query: str) -> str: +# """Fetch data from an external API using delegated access. +# +# Note: Low-level MCP uses AccessContext as a function parameter, +# not retrieved from ctx.get_state(). +# +# Args: +# access_ctx: AccessContext with exchanged tokens +# query: Search query for the external API +# +# Returns: +# Data from the external API or error message +# """ +# if access_ctx.has_errors(): +# return f"Token exchange failed: {access_ctx.get_errors()}" +# +# token = access_ctx.access("https://api.example.com").access_token +# # Use token to call external API +# return f"Fetched data for query: {query}" + + +def main(): + """Entry point for the MCP server.""" + # Wrap the MCP app with Keycard authentication + app = auth_provider.app(mcp) + uvicorn.run(app, host="0.0.0.0", port=8000) + + +if __name__ == "__main__": + main() diff --git a/packages/mcp/examples/hello_world_server/pyproject.toml b/packages/mcp/examples/hello_world_server/pyproject.toml new file mode 100644 index 0000000..1dac1d9 --- /dev/null +++ b/packages/mcp/examples/hello_world_server/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "hello-world-server-lowlevel" +version = "0.1.0" +description = "A minimal MCP server with low-level Keycard authentication" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "keycardai-mcp", + "uvicorn>=0.30.0", +] + +[tool.uv.sources] +keycardai-mcp = { path = "../../", editable = true } + +[project.scripts] +hello-world-server = "main:main"