Edison Watch
Developers

DSPy

Connect a DSPy program to Edison Watch using the MCP Python SDK and dspy.Tool.from_mcp_tool, keeping a stable session per conversation so data-leak protection holds across every turn.

DSPy doesn't ship a dedicated remote-MCP object - you stand up a raw MCP ClientSession over the official Python SDK's streamablehttp_client, then convert each tool with dspy.Tool.from_mcp_tool. Your connection URL carries your API key, so no auth header is needed.

pip install dspy mcp
import asyncio
import os

import dspy
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

dspy.configure(lm=dspy.LM("openai/gpt-5.6-luna"))

class Assistant(dspy.Signature):
    """Answer the user request using the available tools."""
    request: str = dspy.InputField()
    answer: str = dspy.OutputField()

async def main() -> None:
    url = os.environ["EDISON_MCP_URL"]
    async with streamablehttp_client(url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            dspy_tools = [dspy.Tool.from_mcp_tool(session, t) for t in tools.tools]

            react = dspy.ReAct(Assistant, tools=dspy_tools)
            result = await react.acall(request="List my available tools.")
            print(result.answer)

asyncio.run(main())

Set EDISON_MCP_URL to your connection URL, e.g. https://mcp.edison.watch/mcp/<your-api-key>/?client=dspy.

streamablehttp_client yields a 3-tuple (read, write, get_session_id) - not the 2-tuple you get from stdio_client. Discard the third value as _. Copying a stdio example and unpacking two values is a common error. MCP tools are async, so drive the program with react.acall(...).

Keep a stable session across turns to preserve data-leak protection

Send a stable x-edison-conversation-id header on every turn of the same conversation. That header is what keeps Edison's data-leak protection intact across a multi-turn run: Edison tracks lethal-trifecta risk per session, so if each turn looks like a brand-new session, that protection resets - and a later turn can leak data that the accumulated risk should have blocked.

Hosted clients (Claude Code, VS Code) send it automatically. For a custom DSPy program, set the header on streamablehttp_client, keyed to your own conversation or thread id:

# Reuse one stable conversation_id for every turn of the same conversation.
async with streamablehttp_client(
    url,
    headers={"x-edison-conversation-id": conversation_id},
) as (read, write, _):
    ...

Without a stable x-edison-conversation-id, each connection is treated as a fresh session that starts with empty risk state - so risk accumulated on an earlier turn won't be there to block a later exfiltration. The ?client= label is only a dashboard tag, not a session key. Use a unique id per conversation (a UUID is ideal); ids are scoped to your API key, so don't reuse one string for two different conversations.

Optional: the encrypted-secrets header

For servers with zero-knowledge-encrypted secrets, pass headers to streamablehttp_client, alongside x-edison-conversation-id:

async with streamablehttp_client(
    url,
    headers={
        "x-edison-conversation-id": conversation_id,
        "x-edison-secret-key": os.environ["EDISON_SECRET_KEY"],
    },
) as (read, write, _):
    ...