diff --git a/misc/doordash-mcp/README.md b/misc/doordash-mcp/README.md new file mode 100644 index 00000000..0198b92b --- /dev/null +++ b/misc/doordash-mcp/README.md @@ -0,0 +1,235 @@ +# DoorDash MCP Server + +A Model Context Protocol (MCP) server for DoorDash Drive API integration. + +## Overview + +This project provides a FastAPI server that implements the Model Context Protocol (MCP) for DoorDash Drive API, allowing Language Models (LLMs) to interact with DoorDash's delivery services. + +## Installation + +1. Clone this repository +2. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +3. Copy the example environment file and update with your DoorDash credentials: + +```bash +cp example.env .env +``` + +## DoorDash API Integration + +This project integrates with the DoorDash Drive API. For detailed instructions on setup and usage, see [DOORDASH_API.md](DOORDASH_API.md). + +## Running the Server + +Start the server with: + +```bash +uvicorn app.main:app --reload +``` + +The server will be available at http://localhost:8000 + +## MCP Configuration + +The MCP configuration is served at `/mcp-config` endpoint and is also available in the `mcp-config.json` file. + +For IDE integration, an `mcp.json` file is provided that contains server startup configuration. The `print_mcp_config.py` script helps you manage this configuration: + +1. View the current configuration: +```bash +python print_mcp_config.py +``` + +2. Generate a new configuration with your DoorDash credentials: +```bash +python print_mcp_config.py --generate --developer-id "your-id" --key-id "your-key-id" --signing-secret "your-secret" +``` + +3. Generate a template configuration: +```bash +python print_mcp_config.py --generate +``` + +4. See all available options: +```bash +python print_mcp_config.py --help +``` + +## Example Usage + +There are example scripts in the `examples` directory showing how to: + +1. Generate a JWT for DoorDash API authentication +2. Create, get, update, and cancel deliveries + +## Documentation + +API documentation is available at: +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc + +## Features + +- FastAPI-based server with MCP integration +- DoorDash Drive API integration with JWT authentication +- Tools for creating quotes, deliveries, and more +- Uses the official MCP Python SDK from GitHub +- MCP configuration for IDE and client integration + +## Prerequisites + +- Python 3.9+ +- Conda + +## Setup + +1. Clone the repository: + +```bash +git clone https://github.com/yourusername/doordash-mcp.git +cd doordash-mcp +``` + +2. Create and activate the conda environment: + +```bash +conda create -n mcp-server python=3.9 -y +conda activate mcp-server +``` + +3. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +4. Set up environment variables by copying the example file: + +```bash +cp example.env .env +``` + +Edit the `.env` file to add your DoorDash API key (if applicable). + +## Running the Server + +Start the server using Uvicorn: + +```bash +uvicorn app.main:app --reload --port 8000 +``` + +The server will start at http://localhost:8000. + +## API Documentation + +Once the server is running, you can access: + +- API documentation: http://localhost:8000/docs +- ReDoc documentation: http://localhost:8000/redoc +- Health check: http://localhost:8000/health +- MCP configuration: http://localhost:8000/mcp-config + +## MCP Tool Reference + +The server exposes the following MCP tools: + +### create_quote + +Create a delivery quote from DoorDash. + +```python +create_quote( + external_delivery_id: str, + dropoff_address: str, + dropoff_phone_number: str, + **kwargs +) +``` + +### accept_quote + +Accept a delivery quote from DoorDash. + +```python +accept_quote( + external_delivery_id: str, + **kwargs +) +``` + +### create_delivery + +Create a delivery with DoorDash. + +```python +create_delivery( + external_delivery_id: str, + dropoff_address: str, + dropoff_phone_number: str, + **kwargs +) +``` + +### get_delivery + +Get delivery details from DoorDash. + +```python +get_delivery( + external_delivery_id: str +) +``` + +### update_delivery + +Update delivery details in DoorDash. + +```python +update_delivery( + external_delivery_id: str, + **kwargs # Fields to update (e.g., dropoff_instructions) +) +``` + +### cancel_delivery + +Cancel a delivery in DoorDash. + +```python +cancel_delivery( + external_delivery_id: str +) +``` + +## REST API Endpoints + +The server also provides REST API endpoints that mirror the MCP tools functionality: + +- `POST /api/create_quote` +- `POST /api/accept_quote` +- `POST /api/create_delivery` +- `GET /api/delivery/{external_delivery_id}` +- `PATCH /api/delivery/{external_delivery_id}` +- `DELETE /api/delivery/{external_delivery_id}` + +## IDE and Client Integration + +The server provides a JSON configuration file for IDE and client integration. You can access this configuration at: + +``` +http://localhost:8000/mcp-config +``` + +This configuration can be used to automatically integrate the MCP tools with your IDE or client application. The configuration includes: + +- Server name and version +- Available capabilities +- Tool definitions with parameter schemas +- Endpoint information \ No newline at end of file diff --git a/misc/doordash-mcp/app/__init__.py b/misc/doordash-mcp/app/__init__.py new file mode 100644 index 00000000..89d85628 --- /dev/null +++ b/misc/doordash-mcp/app/__init__.py @@ -0,0 +1,5 @@ +""" +Model Context Protocol (MCP) Server Application +""" + +__version__ = '0.1.0' \ No newline at end of file diff --git a/misc/doordash-mcp/app/main.py b/misc/doordash-mcp/app/main.py new file mode 100644 index 00000000..093f5f08 --- /dev/null +++ b/misc/doordash-mcp/app/main.py @@ -0,0 +1,47 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import json +import os + +from app.routers import doordash + +# Create FastAPI application +app = FastAPI(title="Model Context Protocol Server", version="0.1.0") + +# Setup CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register routers +app.include_router(doordash.router, prefix="/api") + +# Get MCP server instance from router +mcp_server = doordash.mcp_server + +@app.get("/") +async def root(): + return {"message": "Welcome to Model Context Protocol Server"} + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +@app.get("/mcp-config") +async def get_mcp_config(): + """Return the MCP configuration for clients and IDEs""" + config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "mcp-config.json") + try: + with open(config_path, 'r') as f: + config = json.load(f) + return JSONResponse(content=config) + except (FileNotFoundError, json.JSONDecodeError) as e: + return JSONResponse( + status_code=500, + content={"error": f"Could not load MCP configuration: {str(e)}"} + ) \ No newline at end of file diff --git a/misc/doordash-mcp/app/routers/__init__.py b/misc/doordash-mcp/app/routers/__init__.py new file mode 100644 index 00000000..41b7ff3a --- /dev/null +++ b/misc/doordash-mcp/app/routers/__init__.py @@ -0,0 +1,3 @@ +""" +Router modules for the MCP server +""" \ No newline at end of file diff --git a/misc/doordash-mcp/app/routers/doordash.py b/misc/doordash-mcp/app/routers/doordash.py new file mode 100644 index 00000000..bdfd5919 --- /dev/null +++ b/misc/doordash-mcp/app/routers/doordash.py @@ -0,0 +1,470 @@ +from fastapi import APIRouter, HTTPException, Path, Query, Depends +from typing import Dict, Any, Optional +from mcp.server.fastmcp import FastMCP + +from app.services.doordash_service import DoordashService +from app.schemas.business import BusinessCreate, BusinessUpdate, BusinessList, BusinessResponse, BusinessListResponse +from app.schemas.store import StoreCreate, StoreUpdate, StoreList, StoreResponse, StoreListResponse + +router = APIRouter(tags=["doordash"]) + +# Initialize DoorDash service +doordash_service = DoordashService() + +# Initialize MCP server +mcp_server = FastMCP("DoorDash MCP Tools") + +# Define MCP tools +@mcp_server.tool() +def create_quote(external_delivery_id: str, dropoff_address: str, dropoff_phone_number: str, **kwargs) -> Dict[str, Any]: + """ + Create a delivery quote from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + dropoff_address: The delivery destination address + dropoff_phone_number: The recipient's phone number + **kwargs: Additional optional parameters + + Returns: + A quote object with pricing and estimated times + """ + try: + data = { + "external_delivery_id": external_delivery_id, + "dropoff_address": dropoff_address, + "dropoff_phone_number": dropoff_phone_number, + **kwargs + } + return doordash_service.create_quote(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def accept_quote(external_delivery_id: str, **kwargs) -> Dict[str, Any]: + """ + Accept a delivery quote from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + **kwargs: Additional optional parameters + + Returns: + A delivery object with tracking information + """ + try: + data = { + "external_delivery_id": external_delivery_id, + **kwargs + } + return doordash_service.accept_quote(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def create_delivery(external_delivery_id: str, dropoff_address: str, dropoff_phone_number: str, pickup_address: str, **kwargs) -> Dict[str, Any]: + """ + Create a delivery with DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + dropoff_address: The delivery destination address + dropoff_phone_number: The recipient's phone number + pickup_address: The pickup location address + **kwargs: Additional optional parameters + + Returns: + A delivery object with tracking information + """ + try: + data = { + "external_delivery_id": external_delivery_id, + "dropoff_address": dropoff_address, + "dropoff_phone_number": dropoff_phone_number, + "pickup_address": pickup_address, + "pickup_external_store_id": "joes-pizza-sf-001", + "pickup_external_business_id": "bryans-business-001", + **kwargs + } + return doordash_service.create_delivery(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def get_delivery(external_delivery_id: str) -> Dict[str, Any]: + """ + Get delivery details from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + + Returns: + The current status and details of the delivery + """ + try: + data = { + "external_delivery_id": external_delivery_id + } + return doordash_service.get_delivery(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def update_delivery(external_delivery_id: str, **kwargs) -> Dict[str, Any]: + """ + Update delivery details in DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + **kwargs: Fields to update (e.g., dropoff_instructions) + + Returns: + The updated delivery object + """ + try: + data = { + "external_delivery_id": external_delivery_id, + **kwargs + } + return doordash_service.update_delivery(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def cancel_delivery(external_delivery_id: str) -> Dict[str, Any]: + """ + Cancel a delivery in DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + + Returns: + Confirmation of cancellation + """ + try: + data = { + "external_delivery_id": external_delivery_id + } + return doordash_service.cancel_delivery(data) + except Exception as e: + raise ValueError(str(e)) + +# Business management MCP tools +@mcp_server.tool() +def create_business(external_business_id: str, name: str, **kwargs) -> Dict[str, Any]: + """ + Create a business in DoorDash. + + Args: + external_business_id: Unique identifier for the business + name: The name of the business + **kwargs: Additional optional parameters (description, external_metadata) + + Returns: + The created business object + """ + try: + data = { + "external_business_id": external_business_id, + "name": name, + **kwargs + } + return doordash_service.create_business(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def get_business(external_business_id: str) -> Dict[str, Any]: + """ + Get details of a business from DoorDash. + + Args: + external_business_id: Unique identifier for the business + + Returns: + The business details + """ + try: + data = { + "external_business_id": external_business_id + } + return doordash_service.get_business(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def update_business(external_business_id: str, **kwargs) -> Dict[str, Any]: + """ + Update business details in DoorDash. + + Args: + external_business_id: Unique identifier for the business + **kwargs: Fields to update (name, description, external_metadata) + + Returns: + The updated business object + """ + try: + data = { + "external_business_id": external_business_id, + **kwargs + } + return doordash_service.update_business(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def list_businesses(**kwargs) -> Dict[str, Any]: + """ + List businesses in DoorDash. + + Args: + **kwargs: Optional filter parameters (activationStatus, continuationToken) + + Returns: + List of businesses + """ + try: + return doordash_service.list_businesses(kwargs if kwargs else None) + except Exception as e: + raise ValueError(str(e)) + +# Store management MCP tools +@mcp_server.tool() +def create_store(external_business_id: str, external_store_id: str, name: str, address: str, **kwargs) -> Dict[str, Any]: + """ + Create a store in DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + name: The name of the store + address: The full address of the store + **kwargs: Additional optional parameters (phone_number) + + Returns: + The created store object + """ + try: + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id, + "name": name, + "address": address, + **kwargs + } + return doordash_service.create_store(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def get_store(external_business_id: str, external_store_id: str) -> Dict[str, Any]: + """ + Get details of a store from DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + + Returns: + The store details + """ + try: + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id + } + return doordash_service.get_store(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def update_store(external_business_id: str, external_store_id: str, **kwargs) -> Dict[str, Any]: + """ + Update store details in DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + **kwargs: Fields to update (name, address, phone_number) + + Returns: + The updated store object + """ + try: + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id, + **kwargs + } + return doordash_service.update_store(data) + except Exception as e: + raise ValueError(str(e)) + +@mcp_server.tool() +def list_stores(external_business_id: str, **kwargs) -> Dict[str, Any]: + """ + List stores for a business in DoorDash. + + Args: + external_business_id: Unique identifier for the business + **kwargs: Optional filter parameters (activationStatus, continuationToken) + + Returns: + List of stores + """ + try: + data = { + "external_business_id": external_business_id, + **kwargs + } + return doordash_service.list_stores(data) + except Exception as e: + raise ValueError(str(e)) + +# REST API endpoints that mirror the MCP tools +@router.post("/create_quote") +async def api_create_quote(data: Dict[str, Any]): + try: + return doordash_service.create_quote(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/accept_quote") +async def api_accept_quote(data: Dict[str, Any]): + try: + return doordash_service.accept_quote(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/create_delivery") +async def api_create_delivery(data: Dict[str, Any]): + try: + return doordash_service.create_delivery(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/delivery/{external_delivery_id}") +async def api_get_delivery(external_delivery_id: str): + try: + data = {"external_delivery_id": external_delivery_id} + return doordash_service.get_delivery(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.patch("/delivery/{external_delivery_id}") +async def api_update_delivery(external_delivery_id: str, data: Dict[str, Any]): + try: + data["external_delivery_id"] = external_delivery_id + return doordash_service.update_delivery(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.delete("/delivery/{external_delivery_id}") +async def api_cancel_delivery(external_delivery_id: str): + try: + data = {"external_delivery_id": external_delivery_id} + return doordash_service.cancel_delivery(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +# Business and Store REST API endpoints +@router.post("/businesses", response_model=BusinessResponse) +async def api_create_business(business: BusinessCreate): + try: + return doordash_service.create_business(business.dict(exclude_none=True)) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/businesses/{external_business_id}", response_model=BusinessResponse) +async def api_get_business(external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$")): + try: + data = {"external_business_id": external_business_id} + return doordash_service.get_business(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.patch("/businesses/{external_business_id}", response_model=BusinessResponse) +async def api_update_business( + data: BusinessUpdate, + external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$") +): + try: + data_dict = data.dict(exclude_none=True) + data_dict["external_business_id"] = external_business_id + return doordash_service.update_business(data_dict) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/businesses", response_model=BusinessListResponse) +async def api_list_businesses( + activation_status: Optional[str] = Query(None, regex=r"^(active|inactive)$"), + continuation_token: Optional[str] = Query(None) +): + try: + params = {} + if activation_status: + params["activationStatus"] = activation_status + if continuation_token: + params["continuationToken"] = continuation_token + return doordash_service.list_businesses(params) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/businesses/{external_business_id}/stores", response_model=StoreResponse) +async def api_create_store( + store: StoreCreate, + external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$") +): + try: + store_dict = store.dict(exclude_none=True) + # Ensure external_business_id in path is used + store_dict["external_business_id"] = external_business_id + return doordash_service.create_store(store_dict) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/businesses/{external_business_id}/stores/{external_store_id}", response_model=StoreResponse) +async def api_get_store( + external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$"), + external_store_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$") +): + try: + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id + } + return doordash_service.get_store(data) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.patch("/businesses/{external_business_id}/stores/{external_store_id}", response_model=StoreResponse) +async def api_update_store( + data: StoreUpdate, + external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$"), + external_store_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$") +): + try: + data_dict = data.dict(exclude_none=True) + # Ensure IDs in path are used + data_dict["external_business_id"] = external_business_id + data_dict["external_store_id"] = external_store_id + return doordash_service.update_store(data_dict) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/businesses/{external_business_id}/stores", response_model=StoreListResponse) +async def api_list_stores( + external_business_id: str = Path(..., regex=r"^[A-Za-z0-9_-]{3,64}$"), + activation_status: Optional[str] = Query(None, regex=r"^(active|inactive)$"), + continuation_token: Optional[str] = Query(None) +): + try: + params = {"external_business_id": external_business_id} + if activation_status: + params["activationStatus"] = activation_status + if continuation_token: + params["continuationToken"] = continuation_token + return doordash_service.list_stores(params) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file diff --git a/misc/doordash-mcp/app/schemas/__init__.py b/misc/doordash-mcp/app/schemas/__init__.py new file mode 100644 index 00000000..ef46193f --- /dev/null +++ b/misc/doordash-mcp/app/schemas/__init__.py @@ -0,0 +1,9 @@ +""" +Schema definitions for the MCP server +""" + +from pydantic import BaseModel + +# Import schemas here +from app.schemas.business import * +from app.schemas.store import * \ No newline at end of file diff --git a/misc/doordash-mcp/app/schemas/business.py b/misc/doordash-mcp/app/schemas/business.py new file mode 100644 index 00000000..53bd55db --- /dev/null +++ b/misc/doordash-mcp/app/schemas/business.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, Field +from typing import Dict, Optional, Any, List + +class BusinessBase(BaseModel): + """Base model for business operations""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + +class BusinessCreate(BusinessBase): + """Model for creating a business""" + name: str = Field(..., description="The name of the business") + description: Optional[str] = Field(None, description="A description of the business") + external_metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata for the business") + +class BusinessUpdate(BaseModel): + """Model for updating a business""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + name: Optional[str] = Field(None, description="The name of the business") + description: Optional[str] = Field(None, description="A description of the business") + external_metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata for the business") + +class BusinessList(BaseModel): + """Model for listing businesses""" + activation_status: Optional[str] = Field(None, description="Filter businesses by activation status ('active' or 'inactive')") + continuation_token: Optional[str] = Field(None, description="Token for pagination") + +class BusinessResponse(BusinessBase): + """Model for business response""" + name: str + description: Optional[str] = None + activation_status: str + created_at: str + last_updated_at: str + is_test: bool + external_metadata: Optional[Dict[str, Any]] = None + +class BusinessListResponse(BaseModel): + """Model for listing businesses response""" + result: List[BusinessResponse] + continuation_token: Optional[str] = None + result_count: int \ No newline at end of file diff --git a/misc/doordash-mcp/app/schemas/store.py b/misc/doordash-mcp/app/schemas/store.py new file mode 100644 index 00000000..56736f8c --- /dev/null +++ b/misc/doordash-mcp/app/schemas/store.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field +from typing import Dict, Optional, Any, List + +class StoreBase(BaseModel): + """Base model for store operations""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + external_store_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID for the store") + +class StoreCreate(BaseModel): + """Model for creating a store""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + external_store_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID for the store") + name: str = Field(..., description="The name of the store") + address: str = Field(..., description="The full address of the store") + phone_number: Optional[str] = Field(None, description="Phone number of the store") + +class StoreUpdate(BaseModel): + """Model for updating a store""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + external_store_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID for the store") + name: Optional[str] = Field(None, description="The name of the store") + address: Optional[str] = Field(None, description="The full address of the store") + phone_number: Optional[str] = Field(None, description="Phone number of the store") + +class StoreList(BaseModel): + """Model for listing stores""" + external_business_id: str = Field(..., pattern=r"^[A-Za-z0-9_-]{3,64}$", description="Unique, caller-selected ID of the business") + activation_status: Optional[str] = Field(None, description="Filter stores by activation status ('active' or 'inactive')") + continuation_token: Optional[str] = Field(None, description="Token for pagination") + +class StoreResponse(StoreBase): + """Model for store response""" + name: str + address: str + phone_number: Optional[str] = None + status: str + is_test: bool + created_at: str + last_updated_at: str + +class StoreListResponse(BaseModel): + """Model for listing stores response""" + result: List[StoreResponse] + continuation_token: Optional[str] = None + result_count: int \ No newline at end of file diff --git a/misc/doordash-mcp/app/services/__init__.py b/misc/doordash-mcp/app/services/__init__.py new file mode 100644 index 00000000..d4c670eb --- /dev/null +++ b/misc/doordash-mcp/app/services/__init__.py @@ -0,0 +1,3 @@ +""" +Service modules for the MCP server +""" \ No newline at end of file diff --git a/misc/doordash-mcp/app/services/doordash_service.py b/misc/doordash-mcp/app/services/doordash_service.py new file mode 100644 index 00000000..67542433 --- /dev/null +++ b/misc/doordash-mcp/app/services/doordash_service.py @@ -0,0 +1,325 @@ +import os +import json +import requests +import jwt +import math +import time +from typing import Dict, Any, Optional +from datetime import datetime + +class DoordashService: + """ + Service class to interact with DoorDash APIs. + This implementation uses the actual DoorDash Drive API. + """ + + def __init__(self): + self.base_url = os.getenv("DOORDASH_API_URL", "https://openapi.doordash.com/drive/v2") + self.developer_base_url = os.getenv("DOORDASH_DEVELOPER_API_URL", "https://openapi.doordash.com/developer/v1") + + # Try to load credentials from separate environment variables + developer_id = os.getenv("DOORDASH_DEVELOPER_ID") + key_id = os.getenv("DOORDASH_KEY_ID") + signing_secret = os.getenv("DOORDASH_SIGNING_SECRET") + + # If separate env vars are provided, use them + if developer_id and key_id and signing_secret: + self.access_key = { + "developer_id": developer_id, + "key_id": key_id, + "signing_secret": signing_secret + } + print("Using DoorDash credentials from individual environment variables") + else: + # Fall back to the combined JSON access key + self.access_key_str = os.getenv("DOORDASH_ACCESS_KEY", "{}") + try: + self.access_key = json.loads(self.access_key_str) + print("Using DoorDash credentials from DOORDASH_ACCESS_KEY") + except json.JSONDecodeError: + self.access_key = {} + print("Warning: Invalid DOORDASH_ACCESS_KEY format") + + def _generate_jwt(self) -> str: + """ + Generate a JSON Web Token (JWT) for API authentication. + """ + if not all(key in self.access_key for key in ["developer_id", "key_id", "signing_secret"]): + raise ValueError("Missing required access key fields") + + # Create the JWT payload + data = { + "aud": "doordash", + "iss": self.access_key["developer_id"], + "kid": self.access_key["key_id"], + "exp": math.floor(time.time() + 300), # 5 minutes expiration + "iat": math.floor(time.time()), + } + + # Create the JWT with the required headers + headers = {"algorithm": "HS256", "header": {"dd-ver": "DD-JWT-V1"}} + + # Sign the JWT + token = jwt.encode( + data, + jwt.utils.base64url_decode(self.access_key["signing_secret"]), + algorithm="HS256", + headers={"dd-ver": "DD-JWT-V1"} + ) + + return token + + def _make_request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None, use_developer_api: bool = False) -> Dict[str, Any]: + """ + Make an HTTP request to the DoorDash API. + """ + # Generate a JWT for authentication + try: + token = self._generate_jwt() + except ValueError as e: + print(f"Error generating JWT: {str(e)}") + return {"error": "Authentication error", "details": str(e)} + + # Set up headers + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + # Construct URL - use developer API for business/store operations + base = self.developer_base_url if use_developer_api else self.base_url + url = f"{base}/{endpoint}" + + # Make the request + try: + if method.lower() == "get": + response = requests.get(url, headers=headers) + elif method.lower() == "post": + response = requests.post(url, headers=headers, json=data) + elif method.lower() == "put": + response = requests.put(url, headers=headers, json=data) + elif method.lower() == "patch": + response = requests.patch(url, headers=headers, data=data) + elif method.lower() == "delete": + response = requests.delete(url, headers=headers) + else: + return {"error": "Unsupported HTTP method"} + + # Handle response + if response.status_code >= 200 and response.status_code < 300: + return response.json() + else: + print(f"API error: {response.status_code} - {response.text}") + return { + "error": f"API error: {response.status_code}", + "details": response.text + } + + except requests.RequestException as e: + print(f"Request error: {str(e)}") + return {"error": "Request failed", "details": str(e)} + + def create_quote(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a delivery quote from DoorDash. + """ + # Validate required parameters + required_fields = ["external_delivery_id", "dropoff_address", "dropoff_phone_number"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + # Make the request + return self._make_request("POST", "quotes", data) + + def accept_quote(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Accept a delivery quote from DoorDash. + """ + # Validate required parameters + if "external_delivery_id" not in data: + raise ValueError("Missing required field: external_delivery_id") + + # The API expects the quote_id in the URL. This should be provided by the client. + if "quote_id" not in data: + raise ValueError("Missing required field: quote_id") + + quote_id = data["quote_id"] + # Remove quote_id from the payload since it's part of the URL + payload = {k: v for k, v in data.items() if k != "quote_id"} + + # Make the request + return self._make_request("POST", f"quotes/{quote_id}/accept", payload) + + def create_delivery(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a delivery with DoorDash. + """ + # Validate required parameters + required_fields = ["external_delivery_id", "dropoff_address", "dropoff_phone_number"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + # Make the request + return self._make_request("POST", "deliveries", data) + + def get_delivery(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Get delivery details from DoorDash. + """ + # Validate required parameters + if "external_delivery_id" not in data: + raise ValueError("Missing required field: external_delivery_id") + + external_id = data["external_delivery_id"] + + # Make the request + return self._make_request("GET", f"deliveries/{external_id}", None) + + def update_delivery(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Update delivery details in DoorDash. + """ + # Validate required parameters + if "external_delivery_id" not in data: + raise ValueError("Missing required field: external_delivery_id") + + external_id = data["external_delivery_id"] + + # Make the request + update_data = {k: v for k, v in data.items() if k != "external_delivery_id"} + return self._make_request("PATCH", f"deliveries/{external_id}", update_data) + + def cancel_delivery(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Cancel a delivery in DoorDash. + """ + # Validate required parameters + if "external_delivery_id" not in data: + raise ValueError("Missing required field: external_delivery_id") + + external_id = data["external_delivery_id"] + + # Make the request + return self._make_request("PUT", f"deliveries/{external_id}/cancel", None) + + # Business and Store Management Methods + + def create_business(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a business in DoorDash. + """ + # Validate required parameters + required_fields = ["external_business_id", "name"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + # Make the request + return self._make_request("POST", "businesses", data, use_developer_api=True) + + def get_business(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Get details of a business from DoorDash. + """ + # Validate required parameters + if "external_business_id" not in data: + raise ValueError("Missing required field: external_business_id") + + external_business_id = data["external_business_id"] + + # Make the request + return self._make_request("GET", f"businesses/{external_business_id}", None, use_developer_api=True) + + def update_business(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Update business details in DoorDash. + """ + # Validate required parameters + if "external_business_id" not in data: + raise ValueError("Missing required field: external_business_id") + + external_business_id = data["external_business_id"] + + # Remove external_business_id from the payload since it's part of the URL + update_data = {k: v for k, v in data.items() if k != "external_business_id"} + + # Make the request + return self._make_request("PATCH", f"businesses/{external_business_id}", update_data, use_developer_api=True) + + def list_businesses(self, data: Dict[str, Any] = None) -> Dict[str, Any]: + """ + List businesses in DoorDash. + """ + # Make the request + return self._make_request("GET", "businesses", data, use_developer_api=True) + + def create_store(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a store in DoorDash. + """ + # Validate required parameters + required_fields = ["external_business_id", "external_store_id", "name", "address"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + external_business_id = data["external_business_id"] + + # Remove external_business_id from the payload since it's part of the URL + store_data = {k: v for k, v in data.items() if k != "external_business_id"} + + # Make the request + return self._make_request("POST", f"businesses/{external_business_id}/stores", store_data, use_developer_api=True) + + def get_store(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Get details of a store from DoorDash. + """ + # Validate required parameters + required_fields = ["external_business_id", "external_store_id"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + external_business_id = data["external_business_id"] + external_store_id = data["external_store_id"] + + # Make the request + return self._make_request("GET", f"businesses/{external_business_id}/stores/{external_store_id}", None, use_developer_api=True) + + def update_store(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Update store details in DoorDash. + """ + # Validate required parameters + required_fields = ["external_business_id", "external_store_id"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + external_business_id = data["external_business_id"] + external_store_id = data["external_store_id"] + + # Remove IDs from the payload since they're part of the URL + update_data = {k: v for k, v in data.items() if k not in ["external_business_id", "external_store_id"]} + + # Make the request + return self._make_request("PATCH", f"businesses/{external_business_id}/stores/{external_store_id}", update_data, use_developer_api=True) + + def list_stores(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + List stores for a business in DoorDash. + """ + # Validate required parameters + if "external_business_id" not in data: + raise ValueError("Missing required field: external_business_id") + + external_business_id = data["external_business_id"] + + # Extract query parameters + query_params = {k: v for k, v in data.items() if k in ["activationStatus", "continuationToken"]} + + # Make the request + return self._make_request("GET", f"businesses/{external_business_id}/stores", query_params, use_developer_api=True) \ No newline at end of file diff --git a/misc/doordash-mcp/example.env b/misc/doordash-mcp/example.env new file mode 100644 index 00000000..0c462271 --- /dev/null +++ b/misc/doordash-mcp/example.env @@ -0,0 +1,17 @@ +# Server Configuration +PORT=8000 +HOST=0.0.0.0 +LOG_LEVEL=INFO + +# DoorDash API Configuration +DOORDASH_API_URL=https://openapi.doordash.com/drive/v2 + +# Option 1: DoorDash Access Key as JSON (recommended for .env files) +# Format: {"developer_id": "your_developer_id", "key_id": "your_key_id", "signing_secret": "your_signing_secret"} +DOORDASH_ACCESS_KEY={"developer_id": "", "key_id": "", "signing_secret": ""} + +# Option 2: DoorDash credentials as separate environment variables +# Uncomment and use these if you prefer separate variables (recommended for mcp.json) +# DOORDASH_DEVELOPER_ID=your_developer_id +# DOORDASH_KEY_ID=your_key_id +# DOORDASH_SIGNING_SECRET=your_signing_secret \ No newline at end of file diff --git a/misc/doordash-mcp/mcp_server.py b/misc/doordash-mcp/mcp_server.py new file mode 100755 index 00000000..2802ea5e --- /dev/null +++ b/misc/doordash-mcp/mcp_server.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python + +# Import required modules +from mcp.server.fastmcp import FastMCP +import os +import sys +import traceback +from app.routers.doordash import doordash_service + +# Initialize MCP server +mcp = FastMCP("DoorDash MCP Tools") + +# Define MCP tools +@mcp.tool() +def create_quote(external_delivery_id: str, dropoff_address: str, dropoff_phone_number: str, **kwargs): + """ + Create a delivery quote from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + dropoff_address: The delivery destination address + dropoff_phone_number: The recipient's phone number + **kwargs: Additional optional parameters + + Returns: + A quote object with pricing and estimated times + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_delivery_id": external_delivery_id, + "dropoff_address": dropoff_address, + "dropoff_phone_number": dropoff_phone_number, + **kwargs, + **processed_kwargs + } + return doordash_service.create_quote(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def accept_quote(external_delivery_id: str, **kwargs): + """ + Accept a delivery quote from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + **kwargs: Additional optional parameters + + Returns: + A delivery object with tracking information + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_delivery_id": external_delivery_id, + **kwargs, + **processed_kwargs + } + return doordash_service.accept_quote(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def create_delivery(external_delivery_id: str, dropoff_address: str, dropoff_phone_number: str, pickup_address: str, **kwargs): + """ + Create a delivery with DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + dropoff_address: The delivery destination address + dropoff_phone_number: The recipient's phone number + pickup_address: The pickup location address + **kwargs: Additional optional parameters + + Returns: + A delivery object with tracking information + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_delivery_id": external_delivery_id, + "dropoff_address": dropoff_address, + "dropoff_phone_number": dropoff_phone_number, + "pickup_address": pickup_address, + **kwargs, + **processed_kwargs + } + return doordash_service.create_delivery(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def get_delivery(external_delivery_id: str): + """ + Get delivery details from DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + + Returns: + The current status and details of the delivery + """ + try: + data = { + "external_delivery_id": external_delivery_id + } + return doordash_service.get_delivery(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def update_delivery(external_delivery_id: str, **kwargs): + """ + Update delivery details in DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + **kwargs: Fields to update (e.g., dropoff_instructions, + dropoff_phone_number, pickup_instructions, etc.) + + Returns: + The updated delivery object + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_delivery_id": external_delivery_id, + **kwargs, + **processed_kwargs + } + return doordash_service.update_delivery(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def cancel_delivery(external_delivery_id: str): + """ + Cancel a delivery in DoorDash. + + Args: + external_delivery_id: Your unique identifier for the delivery + + Returns: + Confirmation of cancellation + """ + try: + data = { + "external_delivery_id": external_delivery_id + } + return doordash_service.cancel_delivery(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +# Business management MCP tools +@mcp.tool() +def create_business(external_business_id: str, name: str, description: str, **kwargs): + """ + Create a business in DoorDash. + + Args: + external_business_id: Unique identifier for the business + name: The name of the business + **kwargs: Additional optional parameters (description, external_metadata) + + Returns: + The created business object + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_business_id": external_business_id, + "description": description, + "name": name, + **kwargs, + **processed_kwargs + } + return doordash_service.create_business(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def get_business(external_business_id: str): + """ + Get details of a business from DoorDash. + + Args: + external_business_id: Unique identifier for the business + + Returns: + The business details + """ + try: + data = { + "external_business_id": external_business_id + } + return doordash_service.get_business(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def update_business(external_business_id: str, **kwargs): + """ + Update business details in DoorDash. + + Args: + external_business_id: Unique identifier for the business + **kwargs: Fields to update (name, description, external_metadata) + + Returns: + The updated business object + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_business_id": external_business_id, + **kwargs, + **processed_kwargs + } + return doordash_service.update_business(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def list_businesses(**kwargs): + """ + List businesses in DoorDash. + + Args: + **kwargs: Optional filter parameters (activationStatus, continuationToken) + + Returns: + List of businesses + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = {**kwargs, **processed_kwargs} + return doordash_service.list_businesses(data if data else None) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +# Store management MCP tools +@mcp.tool() +def create_store(external_business_id: str, external_store_id: str, name: str, address: str, **kwargs): + """ + Create a store in DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + name: The name of the store + address: The full address of the store + **kwargs: Additional optional parameters (phone_number) + + Returns: + The created store object + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id, + "name": name, + "address": address, + **kwargs, + **processed_kwargs + } + return doordash_service.create_store(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def get_store(external_business_id: str, external_store_id: str): + """ + Get details of a store from DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + + Returns: + The store details + """ + try: + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id + } + return doordash_service.get_store(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def update_store(external_business_id: str, external_store_id: str, **kwargs): + """ + Update store details in DoorDash. + + Args: + external_business_id: Unique identifier for the business + external_store_id: Unique identifier for the store + **kwargs: Fields to update (name, address, phone_number) + + Returns: + The updated store object + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_business_id": external_business_id, + "external_store_id": external_store_id, + **kwargs, + **processed_kwargs + } + return doordash_service.update_store(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +@mcp.tool() +def list_stores(external_business_id: str, **kwargs): + """ + List stores for a business in DoorDash. + + Args: + external_business_id: Unique identifier for the business + **kwargs: Optional filter parameters (activationStatus, continuationToken) + + Returns: + List of stores + """ + try: + # Handle the case where kwargs is passed as a string + processed_kwargs = {} + if 'kwargs' in kwargs and isinstance(kwargs['kwargs'], str): + try: + # Try to parse the kwargs string as JSON + import json + parsed_kwargs = json.loads(kwargs['kwargs'].replace('\\', '')) + processed_kwargs = parsed_kwargs + # Remove the original kwargs parameter + del kwargs['kwargs'] + except Exception as e: + print(f"Error parsing kwargs string: {str(e)}") + + # Combine the processed kwargs with any other kwargs + data = { + "external_business_id": external_business_id, + **kwargs, + **processed_kwargs + } + return doordash_service.list_stores(data) + except Exception as e: + traceback.print_exc() + raise ValueError(str(e)) + +# Run the server +if __name__ == "__main__": + # Start both the FastAPI and MCP servers + import uvicorn + from threading import Thread + import time + + # Start FastAPI server in a separate thread + fastapi_thread = Thread( + target=uvicorn.run, + args=("app.main:app",), + kwargs={ + "host": "127.0.0.1", + "port": 8800, + "log_level": "info" + }, + daemon=True + ) + fastapi_thread.start() + + # Allow FastAPI server to start + time.sleep(2) + + # Start MCP server with stdio transport (for Cursor) + try: + # Use the run method with stdio transport for MCP + mcp.run(transport="stdio") + except KeyboardInterrupt: + print("Server stopped") + except Exception as e: + print(f"Error starting MCP server: {e}") + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/misc/doordash-mcp/print_mcp_config.py b/misc/doordash-mcp/print_mcp_config.py new file mode 100755 index 00000000..3f17f395 --- /dev/null +++ b/misc/doordash-mcp/print_mcp_config.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +""" +Utility script to print or generate the MCP configuration file (mcp.json). +""" + +import json +import os +import sys +import argparse +from pathlib import Path + +def print_mcp_config(): + """Print the mcp.json file to the console with proper formatting.""" + script_dir = Path(os.path.dirname(os.path.abspath(__file__))) + mcp_json_path = script_dir / "mcp.json" + + if not mcp_json_path.exists(): + print(f"Error: mcp.json file not found at {mcp_json_path}") + sys.exit(1) + + try: + with open(mcp_json_path, 'r') as f: + config = json.load(f) + + # Print with nice formatting + print(json.dumps(config, indent=2)) + + print("\nMCP Configuration file location:") + print(f"{mcp_json_path}") + + except json.JSONDecodeError: + print(f"Error: mcp.json file contains invalid JSON") + sys.exit(1) + except Exception as e: + print(f"Error reading mcp.json file: {str(e)}") + sys.exit(1) + +def generate_mcp_config(developer_id=None, key_id=None, signing_secret=None, port=8000): + """ + Generate a customized mcp.json file with the provided credentials. + """ + script_dir = Path(os.path.dirname(os.path.abspath(__file__))) + mcp_json_path = script_dir / "mcp.json" + + # Default configuration + config = { + "mcpServers": { + "doordash": { + "command": "python", + "args": [ + "-m", "uvicorn", + "app.main:app", + "--host", "127.0.0.1", + "--port", str(port) + ], + "env": { + "DOORDASH_API_URL": "https://openapi.doordash.com/drive/v2", + "DOORDASH_DEVELOPER_ID": developer_id or "your_developer_id", + "DOORDASH_KEY_ID": key_id or "your_key_id", + "DOORDASH_SIGNING_SECRET": signing_secret or "your_signing_secret", + "LOG_LEVEL": "INFO" + }, + "cwd": str(script_dir), + "configUrl": f"http://localhost:{port}/mcp-config" + } + } + } + + # If all credentials are provided, add a message showing they've been set + if developer_id and key_id and signing_secret: + print("Generating mcp.json with your DoorDash credentials...") + else: + print("Generating default mcp.json template...") + + # Write the configuration to mcp.json + with open(mcp_json_path, 'w') as f: + json.dump(config, f, indent=2) + + print(f"MCP configuration file generated at: {mcp_json_path}") + + # Print the generated configuration + print("\nGenerated configuration:") + print(json.dumps(config, indent=2)) + +def main(): + parser = argparse.ArgumentParser(description='Manage MCP configuration') + parser.add_argument('--print', action='store_true', help='Print the current mcp.json configuration') + parser.add_argument('--generate', action='store_true', help='Generate a new mcp.json configuration') + parser.add_argument('--developer-id', help='DoorDash Developer ID') + parser.add_argument('--key-id', help='DoorDash Key ID') + parser.add_argument('--signing-secret', help='DoorDash Signing Secret') + parser.add_argument('--port', type=int, default=8000, help='Server port (default: 8000)') + + args = parser.parse_args() + + if args.generate: + generate_mcp_config( + developer_id=args.developer_id, + key_id=args.key_id, + signing_secret=args.signing_secret, + port=args.port + ) + elif args.print or len(sys.argv) == 1: # Default action if no arguments provided + print_mcp_config() + else: + parser.print_help() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/misc/doordash-mcp/requirements.txt b/misc/doordash-mcp/requirements.txt new file mode 100644 index 00000000..426d729a --- /dev/null +++ b/misc/doordash-mcp/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.108.0 +uvicorn>=0.25.0 +git+https://github.com/modelcontextprotocol/python-sdk.git +requests>=2.32.0 +python-dotenv>=1.0.0 +pydantic>=2.0.0 +PyJWT>=2.8.0 \ No newline at end of file