fastmcp icon indicating copy to clipboard operation
fastmcp copied to clipboard

Support "dynamic servers"?

Open minznerjosh opened this issue 7 months ago • 5 comments

Enhancement Description

Is it possible to use this framework to build "dynamic servers" as documented in the official TypeScript SDK? https://github.com/modelcontextprotocol/typescript-sdk/tree/main?tab=readme-ov-file#dynamic-servers

Use Case

My organization is interested in building an MCP Server that provides more structure/predefined workflow to LLM to by changing/adding/removing the available tools depending on some internal state.

Proposed Implementation


minznerjosh avatar May 13 '25 15:05 minznerjosh

Hi @minznerjosh! I'm pretty sure adding tools will work out-of-the-box. We don't currently have a public method for removing tools, but I think we could add it as an enhancement (you could try deleting the corresponding entry from mcp._tool_manager._tools if you need this ASAP).

Because of how FastMCP works, I don't think this requires any architectural changes, as we always visit the tool manager on every call to see what tools exist. So I really think it's just adding a public remove method that is user-accessible to the tool manager and the server.

Just to call out - these tool additions/removals would apply to all clients of the server. This is separate from some work we have planned on more tailored include/exclude functionality on a per-client basis.

jlowin avatar May 13 '25 18:05 jlowin

Here's a quick sketch of what works right now to add tools dynamically (here, from calling another tool):

from fastmcp import FastMCP, Client

mcp = FastMCP()

@mcp.tool()
def tool_1()-> None:
    
    @mcp.tool()
    def tool_2():
        return "Hello, world!"
    
async with Client(mcp) as client:
    assert len(await client.list_tools()) == 1

    try:
        await client.call_tool("tool_2")
    except Exception as e:
        print("Tool 2 unavailable")
    
    await client.call_tool("tool_1")
    
    assert len(await client.list_tools()) == 2
    
    result = await client.call_tool("tool_2")
    assert result[0].text == "Hello, world!"
    print("Tool 2 available")

jlowin avatar May 13 '25 18:05 jlowin

I’ve opened a PR to add support for removing tools at runtime: #437

It adds a remove_tool method to both ToolManager and FastMCP, along with tests and cache clearing.

Let me know if you’d like any changes!

davenpi avatar May 13 '25 20:05 davenpi

Thanks @jlowin! Do you know if your provided solution will emit the list changed notification? https://modelcontextprotocol.io/specification/2025-03-26/server/tools#list-changed-notification

minznerjosh avatar May 13 '25 21:05 minznerjosh

It definitely should! I'll look into whether that's been codified in the SDK

jlowin avatar May 13 '25 21:05 jlowin

Here's a quick sketch of what works right now to add tools dynamically (here, from calling another tool):以下是_目前_动态添加工具的快速草图(此处,通过调用另一个工具):

from fastmcp import FastMCP, Client

mcp = FastMCP()

@mcp.tool() def tool_1()-> None:

@mcp.tool()
def tool_2():
    return "Hello, world!"

async with Client(mcp) as client: assert len(await client.list_tools()) == 1

try:
    await client.call_tool("tool_2")
except Exception as e:
    print("Tool 2 unavailable")

await client.call_tool("tool_1")

assert len(await client.list_tools()) == 2

result = await client.call_tool("tool_2")
assert result[0].text == "Hello, world!"
print("Tool 2 available")

I already have an MCP Server and have created a client connection. If I want to dynamically add a tool by providing function code, function name, function description, etc., is it possible?

s0214lx avatar Sep 16 '25 07:09 s0214lx