Skip to content

Instantly share code, notes, and snippets.

@lukestanley
Last active October 21, 2025 01:34
Show Gist options
  • Select an option

  • Save lukestanley/d9819f8f3f387d62dace9e09dd58187b to your computer and use it in GitHub Desktop.

Select an option

Save lukestanley/d9819f8f3f387d62dace9e09dd58187b to your computer and use it in GitHub Desktop.

Revisions

  1. lukestanley revised this gist Oct 21, 2025. 1 changed file with 36 additions and 36 deletions.
    72 changes: 36 additions & 36 deletions dspy_minimal_mcp_demo.py
    Original file line number Diff line number Diff line change
    @@ -23,22 +23,51 @@
    from mcp import ClientSession, StdioServerParameters
    from mcp.client.sse import sse_client
    from mcp.client.stdio import stdio_client

    from icecream import ic

    from keys import OPENROUTER_API_KEY

    LLM=dspy.LM("openrouter/openai/gpt-5-mini", api_key=OPENROUTER_API_KEY, temperature=1.0, max_tokens = 16000)
    LLM=dspy.LM("openrouter/openai/gpt-5-nano", api_key=OPENROUTER_API_KEY, temperature=1.0, max_tokens = 16000)

    # Multiple kinds of MCP servers easily added here:
    # Easily set multiple kinds of MCP servers:
    SERVERS = [
    # "npx -y @modelcontextprotocol/server-memory",
    "npx -y @modelcontextprotocol/server-memory",
    "https://mcp.deepwiki.com/sse",
    # "uvx mcp-server-fetch",
    # "npx -y @modelcontextprotocol/server-sequential-thinking",
    "uvx mcp-server-fetch",
    "npx -y @modelcontextprotocol/server-sequential-thinking",
    "npx -y @playwright/mcp@latest",
    ]

    class Ask(dspy.Signature):
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

    source = open(__file__).read()

    # A demo question:
    q=f"""Script:
    <file>
    {source}
    </file>
    1. Explain what the script does in plain English with emojis using up to 30 words.
    2. Summarise your tools with raw tool names, e.g: read_wiki_structure first.
    3. Summarise the top story that is on BOTH YC News and BBC News.
    """

    ic(q) # Lists the MCP server tools

    # A more simple API interface:
    async def main():
    dspy.configure(lm=LLM, callbacks=[DSPyLoggingCallback()])

    async with get_mcp_tool_session(SERVERS) as (tools, call_tool):
    agent = dspy.ReAct(Ask, tools=tools)
    output = await agent.acall(
    question=q
    )
    print(output.answer)

    # The wrapper:
    @asynccontextmanager
    async def get_mcp_tool_session(servers: list[str]):
    async with AsyncExitStack() as stack:
    @@ -70,34 +99,5 @@ def on_module_end(self, call_id, outputs, exception):
    print(f" {k}: {v}")
    print("")

    # Currently, a raw and unoptimised signature:
    class Ask(dspy.Signature):
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

    source = open(__file__).read()

    q=f"""Script:
    <file>
    {source}
    </file>
    1. Explain what the script does in plain English with emojis using up to 30 words.
    2. Summarise your tools with raw tool names, e.g: read_wiki_structure first.
    3. Summarise the top story that is on BOTH YC News and BBC News.
    """

    ic(q) # Lists the MCP server tools

    # A more simple API interface:
    async def main():
    dspy.configure(lm=LLM, callbacks=[DSPyLoggingCallback()])

    async with get_mcp_tool_session(SERVERS) as (tools, call_tool):
    agent = dspy.ReAct(Ask, tools=tools)
    output = await agent.acall(
    question=q
    )
    print(output.answer)

    if __name__ == "__main__":
    asyncio.run(main())
    asyncio.run(main())
  2. lukestanley created this gist Oct 21, 2025.
    103 changes: 103 additions & 0 deletions dspy_minimal_mcp_demo.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,103 @@
    #!/usr/bin/env -S uv run
    # Uses the inline script metadata Python Enhancment Proposal (PEP 723)
    # To install UV see: https://docs.astral.sh/uv/getting-started/installation/
    # To run with UV: uv run dspy_minimal_mcp_demo.py

    # /// script
    # requires-python = ">=3.11"
    # dependencies = [
    # "dspy>=3.0.2",
    # "mcp>=1.2.0",
    # "litellm>=1.76.0",
    # "icecream"
    # ]
    # ///

    import asyncio
    import shlex
    from contextlib import asynccontextmanager, AsyncExitStack

    import dspy
    from dspy import Tool
    from dspy.utils.callback import BaseCallback
    from mcp import ClientSession, StdioServerParameters
    from mcp.client.sse import sse_client
    from mcp.client.stdio import stdio_client

    from icecream import ic

    from keys import OPENROUTER_API_KEY

    LLM=dspy.LM("openrouter/openai/gpt-5-mini", api_key=OPENROUTER_API_KEY, temperature=1.0, max_tokens = 16000)

    # Multiple kinds of MCP servers easily added here:
    SERVERS = [
    # "npx -y @modelcontextprotocol/server-memory",
    "https://mcp.deepwiki.com/sse",
    # "uvx mcp-server-fetch",
    # "npx -y @modelcontextprotocol/server-sequential-thinking",
    "npx -y @playwright/mcp@latest",
    ]

    @asynccontextmanager
    async def get_mcp_tool_session(servers: list[str]):
    async with AsyncExitStack() as stack:
    all_tools: list[Tool] = []

    for spec in servers:
    if spec.startswith("http://") or spec.startswith("https://"):
    read, write = await stack.enter_async_context(sse_client(url=spec))
    else:
    parts = shlex.split(spec)
    params = StdioServerParameters(command=parts[0], args=parts[1:], env=None)
    read, write = await stack.enter_async_context(stdio_client(params))

    session = await stack.enter_async_context(ClientSession(read, write))
    await session.initialize()
    tool_list = await session.list_tools()
    all_tools.extend(Tool.from_mcp_tool(session, desc) for desc in tool_list.tools)

    async def call_tool(name: str, **kwargs):
    tool = next(t for t in all_tools if t.name == name)
    return await tool.acall(**kwargs)

    yield all_tools, call_tool

    # Just for logging:
    class DSPyLoggingCallback(BaseCallback):
    def on_module_end(self, call_id, outputs, exception):
    for k, v in outputs.items():
    print(f" {k}: {v}")
    print("")

    # Currently, a raw and unoptimised signature:
    class Ask(dspy.Signature):
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

    source = open(__file__).read()

    q=f"""Script:
    <file>
    {source}
    </file>
    1. Explain what the script does in plain English with emojis using up to 30 words.
    2. Summarise your tools with raw tool names, e.g: read_wiki_structure first.
    3. Summarise the top story that is on BOTH YC News and BBC News.
    """

    ic(q) # Lists the MCP server tools

    # A more simple API interface:
    async def main():
    dspy.configure(lm=LLM, callbacks=[DSPyLoggingCallback()])

    async with get_mcp_tool_session(SERVERS) as (tools, call_tool):
    agent = dspy.ReAct(Ask, tools=tools)
    output = await agent.acall(
    question=q
    )
    print(output.answer)

    if __name__ == "__main__":
    asyncio.run(main())