Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: render method for an MCP server #1

Closed
wants to merge 6 commits into from
Closed

Conversation

geelen
Copy link
Owner

@geelen geelen commented Mar 28, 2025

Currently, the design of the McpServer makes it easy to define servers whose resources/tools/prompts are static and known ahead of time:

const server = new McpServer({ name: '...', version: '1.0.0' })

// Attach prompts/resources/tools to the server before opening to connections
server.resource(`counter`, `mcp://resource/counter`, (uri) => {
  return { contents: [{ uri: uri.href, text: String(1) }] }
})
server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({
  content: [{ type: 'text', text: String(a + b) }],
}))
server.prompt('review-code', { code: z.string() }, ({ code }) => ({
  messages: [{ role: 'user', content: { type: 'text', text: `Please review this code:\n\n${code}` } }],
}))

// Connect to transport
await server.connect(transport)

There's nothing stopping you from adding tools/prompts/resources after connecting, but it also doesn't broadcast list_changed events if you were to do so. So there's not really a good way to make a "dynamic" McpServer without dropping down a level and building your own abstraction using a Server.

I've been looking for a neat API which can make these servers dynamic without needing a complete restructure of the class. This is the best I've come up with: adding the concept of a render function where the output is diffed to the previous version to emit change events:

const server = new McpServer(
  {
    name: '...',
    version: '1.0.0',
  },
  {
    // This function describes the total surface area of the McpServer. 
    // Nothing can be added outside of it
    render: ({ resource, tool, prompt }) => {
      resource(`counter`, `mcp://resource/counter`, /* ... */)
      tool('add', { a: z.number(), b: z.number() }, , /* ... */)
      prompt('review-code', { code: z.string() }, /* ... */)
    },
  },
)

// Call render at least once before connecting
server.render()
// Connect as normal
await server.connect(transport)

// Rerunning render recalculates the whole McpServer, and emits events if anything change
server.render()

As written, the render() function could observe some external state to choose to hide/show/tweak aspects of the server, but I think it makes more sense if render takes an argument:

type RenderArgs = {
  state: 'loading' | 'loaded'
  userTier: 'free' | 'paid'
}

const server = new McpServer<RenderArgs>(
  {
    name: '...',
    version: '1.0.0',
  },
  {
    render: ({ resource, tool, prompt }, args) => {
      prompt('review-code', { code: z.string() }, /* ... */)

      if (args.state === 'loading') return

      resource(`counter`, `mcp://resource/counter`, /* ... */)

      if (args.userTier === 'paid') {
        tool('add', { a: z.number(), b: z.number() }, , /* ... */)
      }
    },
  },
)

server.render({ state: 'loading' })
server.connect(transport)

const user = await loadUser()
// Emits relevant update events automatically
server.render({ state: 'loaded', userTier: user.tier })

This feels like the most natural way to take the current API and make it dynamic, though there's plenty of alternatives.

Anyway, I thought I'd raise this as a draft PR to see if there was any interest in the API itself, before I finish it off. Currently it needs a smarter diffing algorithm (currently only looks for things being added/removed, but a change of description should probably fire an event too), as well as some tests.

@geelen geelen closed this Mar 28, 2025
@geelen
Copy link
Owner Author

geelen commented Mar 30, 2025

Closed in favour of modelcontextprotocol#233

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant