New

Build and Deploy a Custom MCP Server on SingleStore Cloud Functions

Notebook


SingleStore Notebooks

Build and Deploy a Custom MCP Server on SingleStore Cloud Functions

Note

This notebook requires a Standard or Premium workspace. Cloud Functions and database creation are not available on the Free Starter Workspace tier.

Step 1: Setup and Configuration

We will start by setting up our environment and configuring the necessary components for our custom MCP server. This includes creating a new database, setting up product and orders table, and inserting mock data that will be exposed in a structured format through MCP tools.

In [1]:

1%%sql2DROP DATABASE IF EXISTS mcp_demo;3CREATE DATABASE mcp_demo;4
5USE mcp_demo;6
7CREATE TABLE products (8    id INT PRIMARY KEY,9    name VARCHAR(255),10    category VARCHAR(100),11    price DECIMAL(10,2),12    stock INT,13    description TEXT14);15
16INSERT INTO products VALUES17(1, 'SingleStore T-Shirt', 'Apparel', 29.99, 150, 'Comfortable cotton tee with the SingleStore logo'),18(2, 'Developer Hoodie', 'Apparel', 59.99, 75, 'Warm hoodie for late-night coding sessions'),19(3, 'Mechanical Keyboard', 'Electronics', 149.99, 30, 'Cherry MX Blue switches, RGB backlight'),20(4, 'Ergonomic Mouse', 'Electronics', 79.99, 50, 'Vertical design reduces wrist strain'),21(5, 'Standing Desk', 'Furniture', 499.99, 20, 'Electric height-adjustable desk'),22(6, 'Monitor Light Bar', 'Electronics', 39.99, 100, 'Reduce eye strain with screen-mounted lighting'),23(7, 'Cable Management Kit', 'Accessories', 19.99, 200, 'Keep your desk clean and organized'),24(8, 'Laptop Stand', 'Accessories', 34.99, 80, 'Aluminum, adjustable height and angle'),25(9, 'Noise Cancelling Headphones', 'Electronics', 299.99, 25, 'ANC with 30-hour battery life'),26(10, 'Webcam HD Pro', 'Electronics', 89.99, 60, '1080p with auto-focus and built-in mic');27
28CREATE TABLE orders (29    id INT PRIMARY KEY,30    product_id INT,31    customer_name VARCHAR(255),32    quantity INT,33    order_date DATE,34    status VARCHAR(50)35);36
37INSERT INTO orders VALUES38(1, 1, 'Alice', 2, '2025-04-01', 'shipped'),39(2, 3, 'Bob', 1, '2025-04-02', 'delivered'),40(3, 5, 'Charlie', 1, '2025-04-03', 'processing'),41(4, 2, 'Diana', 3, '2025-04-04', 'shipped'),42(5, 9, 'Eve', 1, '2025-04-05', 'delivered'),43(6, 4, 'Frank', 2, '2025-04-06', 'processing'),44(7, 6, 'Grace', 4, '2025-04-07', 'shipped'),45(8, 10, 'Hank', 1, '2025-04-08', 'delivered'),46(9, 7, 'Ivy', 5, '2025-04-09', 'shipped'),47(10, 8, 'Jack', 1, '2025-04-10', 'processing');

Step 2: Install Dependencies

We use the fastmcp library (FastMCP 2.x). It exposes a clean http_app() method that returns an ASGI app whose routes and lifespan can be composed directly into a FastAPI app — which is exactly what the SingleStore Cloud Function harness (singlestoredb.apps.run_function_app) expects.

Why the explicit pins and the sys.modules purge below. The Aura runtime pre-loads anyio at kernel startup (via singlestoredb.apps). When we pip install fastmcp, pip upgrades anyio on disk to a version that has AnyByteStreamConnectable, but sys.modules still holds the old anyio.abc from kernel startup. The next fresh import anyio.streams.text then does from ..abc import AnyByteStreamConnectable, finds the stale anyio.abc in sys.modules, and blows up with ImportError: cannot import name 'AnyByteStreamConnectable' from 'anyio.abc'. We also pin starlette<0.47 so the preinstalled fastapi 0.115.12 stops complaining (it requires starlette<0.47.0,>=0.40.0).

| Package | Constraint | Reason | |---|---|---| | anyio | >=4.10 | mcp (dependency of fastmcp) imports AnyByteStreamConnectable, added in anyio 4.10. | | starlette | <0.47.0,>=0.40.0 | The preinstalled FastAPI 0.115.12 requires this range; fastmcp/mcp want starlette>=0.27 — both satisfied here. | | fastmcp | (latest) | Provides FastMCP.http_app() and composable ASGI routes/lifespan. |

In [2]:

1%pip install --quiet \2    "anyio>=4.10" \3    "starlette<0.47.0,>=0.40.0" \4    "fastmcp"5
6# Clear any stale imports from the runtime's default versions of these7# libraries so the freshly installed ones are picked up without a kernel8# restart. `anyio` in particular is loaded early by `singlestoredb.apps`,9# so its `abc` submodule is cached before pip upgrades the files on disk.10import sys11for _mod in list(sys.modules):12    if any(13        _mod == p or _mod.startswith(p + ".")14        for p in ("mcp", "anyio", "starlette", "fastmcp", "sniffio", "httpx")15    ):16        del sys.modules[_mod]17
18
19# Verify the versions we actually loaded.20from importlib.metadata import version as _pkg_version21import fastmcp, mcp, anyio, starlette  # force import; missing pkg fails loudly22for _pkg in ("fastmcp", "mcp", "anyio", "starlette"):23    print(f"{_pkg:<9} == {_pkg_version(_pkg)}")24print("Dependencies ready.")

If an import in a later cell still fails (e.g. you re-ran Step 2 and cells in between), use Kernel → Restart and re-run the notebook from the top. That clears any C-extension state that del sys.modules cannot. The pyopenssl/cryptography warning is a pre-existing state of the Aura runtime and is safe to ignore.

Step 3: Create the Database Connection

Set up a reusable connection to the mcp_demo database using the official singlestoredb Python SDK. The connection_url variable is automatically injected by the SingleStore Notebook runtime when a deployment is selected.

In [3]:

1import singlestoredb as s22import logging3import json4
5logging.basicConfig(level=logging.INFO)6
7def run_query(query: str, params: dict | None = None) -> list[dict]:8    """Execute a SQL query and return results as a list of dicts."""9    with s2.connect(connection_url, database="mcp_demo") as conn:10        with conn.cursor() as cur:11            cur.execute(query, params or {})12            if cur.description:13                columns = [desc[0] for desc in cur.description]14                rows = cur.fetchall()15                return [dict(zip(columns, row)) for row in rows]16            return []17
18logging.info("Database connection ready.")

Step 4: Build the MCP Server

Create a FastMCP instance and register tools with @mcp.tool(). Each tool's docstring and type hints are surfaced to the MCP client (Claude) as the tool's description and schema.

In [4]:

1from fastmcp import FastMCP2
3mcp = FastMCP(name="SingleStore Product Catalog")4
5
6@mcp.tool()7def health() -> str:8    """9    Liveness check for the SingleStore MCP server. Returns 'ok' if the10    server is reachable and SQL connections are working.11    """12    try:13        run_query("SELECT 1 AS ok")14        return "ok"15    except Exception as e:16        return f"error: {e}"17
18
19@mcp.tool()20def search_products(query: str, category: str = "") -> str:21    """22    Search the product catalog by name or description.23    Optionally filter by category.24
25    Args:26        query: Search term to match against product name or description27        category: Optional category filter (e.g., 'Electronics', 'Apparel')28    """29    sql = """30        SELECT id, name, category, price, stock, description31        FROM products32        WHERE (name LIKE %(query)s OR description LIKE %(query)s)33    """34    params = {"query": f"%{query}%"}35    if category:36        sql += " AND category = %(category)s"37        params["category"] = category38
39    results = run_query(sql, params)40    if not results:41        return f"No products found matching '{query}'."42    return json.dumps(results, indent=2, default=str)43
44
45@mcp.tool()46def get_product_details(product_id: int) -> str:47    """48    Get full details for a specific product by its ID.49
50    Args:51        product_id: The numeric ID of the product52    """53    results = run_query(54        "SELECT * FROM products WHERE id = %(product_id)s",55        {"product_id": product_id},56    )57    if not results:58        return f"No product found with ID {product_id}."59    return json.dumps(results[0], indent=2, default=str)60
61
62@mcp.tool()63def check_inventory(category: str = "") -> str:64    """65    Check current inventory levels. Optionally filter by category.66    Returns products sorted by stock level (lowest first).67
68    Args:69        category: Optional category filter70    """71    sql = "SELECT name, category, stock, price FROM products"72    params = {}73    if category:74        sql += " WHERE category = %(category)s"75        params["category"] = category76    sql += " ORDER BY stock ASC"77    return json.dumps(run_query(sql, params), indent=2, default=str)78
79
80@mcp.tool()81def get_recent_orders(limit: int = 10) -> str:82    """83    Get the most recent orders with product details.84
85    Args:86        limit: Number of orders to return (default 10)87    """88    sql = """89        SELECT o.id AS order_id, p.name AS product,90               o.customer_name, o.quantity, o.order_date, o.status91        FROM orders o92        JOIN products p ON o.product_id = p.id93        ORDER BY o.order_date DESC94        LIMIT %(limit)s95    """96    return json.dumps(run_query(sql, {"limit": limit}), indent=2, default=str)97
98
99@mcp.tool()100def get_sales_summary() -> str:101    """102    Get a summary of sales by category, including103    total revenue and units sold.104    """105    sql = """106        SELECT p.category,107               COUNT(o.id) AS total_orders,108               SUM(o.quantity) AS total_units,109               ROUND(SUM(o.quantity * p.price), 2) AS total_revenue110        FROM orders o111        JOIN products p ON o.product_id = p.id112        GROUP BY p.category113        ORDER BY total_revenue DESC114    """115    return json.dumps(run_query(sql), indent=2, default=str)116
117
118logging.info("MCP server created with tools registered.")

Step 5: Wrap the MCP Server in FastAPI

Cloud Functions are served by FastAPI. fastmcp gives us an ASGI app via mcp.http_app(path="/mcp"); we pass its routes and lifespan directly into a new FastAPI instance — and we deliberately do not add any additional @app.get/@app.post endpoints.

Why no extra FastAPI endpoints? The SingleStore Cloud Function gateway builds its route map from what FastAPI exposes via openapi.json. If the app declares no user-defined APIRoutes, the gateway passes every request through to uvicorn, so your fastmcp-registered /mcp route serves requests normally. The moment you add a single @app.get("/health"), the gateway flips to a filtering mode and only proxies paths that appear in openapi.json — which excludes plain Starlette Route objects (like the one fastmcp registers for /mcp). Any unknown path then gets a 307 slash-toggle fallback from the gateway, which also strips the /functions/<id> prefix and loops. To keep /mcp reachable, we keep the FastAPI app pristine — the MCP protocol itself handles tool discovery (tools/list JSON-RPC method) and liveness (just a plain initialize works).

Do not use app.mount("/mcp", ...): Mount entries are also dropped by the same gateway filtering.

In [5]:

1from fastapi import FastAPI, Request2from fastapi.responses import Response3
4# Get fastmcp's ASGI handler — this is the real MCP protocol implementation5# that handles initialize, tools/list, tools/call, SSE streaming, etc.6mcp_asgi_app = mcp.http_app(path="/mcp")7_fastmcp_mcp_route = next(8    r for r in mcp_asgi_app.routes if getattr(r, "path", None) == "/mcp"9)10_mcp_asgi_handler = _fastmcp_mcp_route.app11
12app = FastAPI(13    title="SingleStore MCP Server",14    description="A custom MCP server deployed as a SingleStore Cloud Function",15    version="1.0.0",16    lifespan=mcp_asgi_app.lifespan,17)18
19
20class ASGIProxyResponse(Response):21    """A FastAPI/Starlette `Response` whose `__call__` delegates the entire22    ASGI interaction (headers, body, SSE streaming) to another ASGI app.23
24    Why: the SingleStore Cloud Function gateway only proxies requests to25    paths it sees in `openapi.json`. `fastmcp` registers `/mcp` as a plain26    Starlette `Route`, which FastAPI's schema generator never picks up —27    so the gateway 307-loops requests (`/mcp` ↔ `/mcp/`, prefix stripped).28    Adding an `@app.post("/mcp")` stub fixes the gateway's view but then29    the stub intercepts the request instead of `fastmcp`. Returning this30    proxy response from the stub hands the ASGI scope/receive/send off to31    `fastmcp` untouched, so SSE streaming and all MCP protocol semantics32    work end-to-end.33
34    We buffer the request body at handler time (via `request.body()`) and35    replay it on fastmcp's first `receive()` call. Subsequent `receive()`36    calls fall through to the real client connection so SSE keepalive and37    disconnect signals work normally.38    """39
40    def __init__(self, target, scope, body: bytes, real_receive):41        super().__init__(content=b"", status_code=200)42        self._target = target43        self._scope = scope44        self._body = body45        self._real_receive = real_receive46
47    async def __call__(self, scope, receive, send):48        replayed = False49
50        async def _receive():51            nonlocal replayed52            if not replayed:53                replayed = True54                return {"type": "http.request", "body": self._body, "more_body": False}55            return await self._real_receive()56
57        await self._target(self._scope, _receive, send)58
59
60@app.post("/mcp")61@app.get("/mcp", include_in_schema=False)62@app.delete("/mcp", include_in_schema=False)63async def mcp_endpoint(request: Request):64    """MCP Streamable HTTP endpoint — delegates to fastmcp's ASGI handler."""65    body = await request.body()66    return ASGIProxyResponse(67        _mcp_asgi_handler, request.scope, body, request.receive68    )

Step 6: Start the Cloud Function Server

Run the FastAPI app using the SingleStore Cloud Function SDK. This gives you a temporary interactive URL to test against before publishing.

In [6]:

1import singlestoredb.apps as apps2
3connection_info = await apps.run_function_app(app)

You'll see a temporary URL and a Swagger UI. Confirm /health returns {"status":"ok",...} before moving on.

Step 7: Publish as a Cloud Function

Once the interactive run works:

  1. Click Publish in the top-right corner of the notebook.

  2. Select Cloud Function.

  3. Give it a name, e.g. mcp-product-catalog.

  4. Select your SingleStore deployment (workspace).

  5. Choose a runtime size (Small is fine for testing).

  6. Click Publish.

After a few moments the status changes to Active. You now have a stable HTTPS URL like:

https://apps.us-east-1.cloud.singlestore.com/functions/<function-id>/

The MCP endpoint is at:

https://apps.us-east-1.cloud.singlestore.com/functions/<function-id>/mcp

Step 8: Create a Container App API Key

Your Cloud Function is protected by JWT authentication. To call it you need an API Key.

  1. Navigate to Container Services > Cloud Functions in the Cloud Portal.

  2. Select your mcp-product-catalog function.

  3. Click View API Keys in the top-right.

  4. Click Create API Key.

  5. Enter a name (e.g., claude-mcp-key) and set an expiration date.

  6. Click Create API Key and copy the key immediately — it is shown only once.

You'll use this key as a Bearer token: Authorization: Bearer <YOUR_API_KEY>.

Step 9: Quick Test with curl

The deployed MCP server exposes a single path, /mcp, which implements the MCP streamable-HTTP transport. Per the MCP spec it accepts POST for JSON-RPC messages, GET to open a server-sent-events stream, and DELETE to close a session. Use initialize as the liveness check.

export BASE_URL="https://apps.us-east-1.cloud.singlestore.com/functions/<function-id>"
export MCP_URL="$BASE_URL/mcp"
export API_KEY="<your-api-key>"

# MCP initialize (POST) — should return the server's capabilities
# and an `mcp-session-id` response header.
curl -sS -i -X POST "$MCP_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "capabilities": {},
      "clientInfo": {"name": "curl-test", "version": "1.0.0"}
    }
  }'

A successful response looks like:

HTTP/2 200
content-type: text/event-stream
mcp-session-id: <session-id>
...
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{...},"serverInfo":{"name":"SingleStore Product Catalog","version":"..."}}}

For a simple liveness check you can also call the health tool once initialized, or inspect tool listings via tools/list.

Troubleshooting

  • 307 Temporary Redirect with Location: http://apps.us-east-1.cloud.singlestore.com/mcp/ (prefix stripped): the SingleStore gateway has put the function into "filtered" mode because an @app.get/@app.post/etc. endpoint was added in Step 5. Remove any such endpoints so the FastAPI app has no user-defined APIRoutes, rerun Steps 4–6, and republish.

  • 401 Unauthorized: the Container App API Key has expired or is wrong. Generate a new one (Step 8) and re-export API_KEY.

Step 10: Connect Claude Code

Claude Code supports remote MCP servers natively. Add your server with one command:

claude mcp add singlestore-catalog \
  --transport http \
  "$MCP_URL" \
  --header "Authorization: Bearer $API_KEY"

Start Claude Code and try prompts like:

  • "Search for products related to keyboards."

  • "What's in stock in the Electronics category?"

  • "Show me the recent orders."

  • "Give me a sales summary by category."

Claude discovers the tools from your MCP server automatically and calls them to answer your questions.

Adding More Tools

Extending this is just adding another @mcp.tool() function and republishing.

Details


About this Template

Learn how to write a custom MCP server and deploy it remotelyusing singlestore cloud functions.

This Notebook can be run in Standard and Enterprise deployments.

Tags

advancednotebookspython

See Notebook in action

Launch this notebook in SingleStore and start executing queries instantly.

License

This Notebook has been released under the Apache 2.0 open source license.