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

Declarative, high-level MCP server API #116

Open
wong2 opened this issue Jan 6, 2025 · 13 comments
Open

Declarative, high-level MCP server API #116

wong2 opened this issue Jan 6, 2025 · 13 comments
Labels
enhancement New feature or request

Comments

@wong2
Copy link

wong2 commented Jan 6, 2025

Just like the Python SDK integrates FastMCP modelcontextprotocol/python-sdk#106
I think the TypeScript SDK can also integrate LiteMCP from the TypeScript ecosystem. It was inspired by FastMCP and offers a similar high-level API for developing MCP servers in TypeScript.

@wong2 wong2 added the enhancement New feature or request label Jan 6, 2025
@jspahrsummers
Copy link
Member

We're definitely aware of LiteMCP, as well as similar projects easy-mcp and fastmcp.

I do think the SDK needs improvement, specifically for ease of use, but I'm not convinced that these approaches offer much more than the SDK as authored today. The huge advantage of FastMCP in Python is its declarative nature.

We probably do want to make the TypeScript SDK more Express-like in usage, but this seems a comparatively minor change. If anyone has ideas for a declarative SDK, though, that'd be interesting!

@jspahrsummers jspahrsummers changed the title Integrating LiteMCP for high-level MCP server API Declarative, high-level MCP server API Jan 6, 2025
@zcaceres
Copy link

@jspahrsummers I recently pushed up a proof of concept of a declarative API in EasyMCP.

// This decorator creates a root for the MCP
@Root("/my-root-dir")
class ZachsMCP extends EasyMCP {
  /**
  You can declare a with zero configuration. Relevant types and plumbing will be inferred and handled.

  By default, the *name* of the Tool will be the name of the method.
  */
  @Tool()
  simpleFunc(nickname: string, height: number) {
    return `${nickname} of ${height} height`;
  }
}

Here's a video of it.

It uses Reflect and essentially hacks on the function .prototype to work. There are various rough edges. For example, you can't pass in argument objects when declaring functions.

But it made me believe that this is possible.

With some polish, I think there's a version that could work now as an experimental API.

My conclusion is that the next place to investigate is a custom compile time Typescript transformer. It would likely lead to elegant output with precise types rather than the brittle inference my current decorator supports.

@jspahrsummers
Copy link
Member

Yeah, I was also exploring decorators—but they're unfortunately quite unconventional in JS, and require an OO style to utilize too (can't apply them to free functions).

A TypeScript transformer seems promising, so long as it's not the only way to access key functionality (not everyone wants to use TS).

@zcaceres
Copy link

zcaceres commented Jan 22, 2025

Yeah I don't think the language lends itself in the same way as Python.

IMO the primary savings are in the input declarations. So any pattern that can infer the types and generate those configs would be a net win.

I also considered whether a React-like declarative API could be an alternative to Decorators -- sure to elicit strong opinions 😁

<MCPServer>
<Tool />
<Resource url={} fn={} />
</MCPServer>

As for a transformer, I see it as a way to achieve maximum abstraction but where the default could always be a more verbose Express-like pattern.

@jspahrsummers
Copy link
Member

Actually, JSX isn't a terrible idea! The thought didn't even cross my mind, but I kinda like it. 😂

@zcaceres
Copy link

Actually, JSX isn't a terrible idea! The thought didn't even cross my mind, but I kinda like it. 😂

I agree! But I'm not the one who's going to have to listen to the anti-JSX complaining in the Issues section 😂

@jspahrsummers
Copy link
Member

Just playing around with the quick start example in imagined JSX:

function server() {
  const add = async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }]
  });

  const loadResource = async (uri, { name }) => ({
    contents: [{
      uri: uri.href,
      text: `Hello, ${name}!`
    }]
  })

  return (
    <McpServer name="Demo" version="1.0.0">
      <Tool name="add" input={{ a: z.number(), b: z.number() }} oncall={add} />
      <Resources name="greeting" template="greeting://{name}" onload={loadResource}>
        <Resource uri="greeting://Alice" name="Alice" />
        <Resource uri="greeting://Bob" name="Bob" />
      </Resources>
    </McpServer>
  );
}

Or maybe tool arguments could be defined more naturally like this:

<Tool name="add" oncall={add}>
  <Input name="a" type={z.number()} />
  <Input name="b" type={z.number()} />
</Tool>

I dunno if it's better or not. Will noodle on it.

@zcaceres
Copy link

I think it's cool. And to the extent the JSX is sugar around the Express-like API, it could be an interesting other form factor.

I also like the use of zod to abstract some of the input schema generation.

@ondrsh
Copy link

ondrsh commented Jan 24, 2025

The cool thing about TS transformers is having access to JSDocs. This means users can write normal code+docs and the transformer generates the complete JSON schemas, including tool and parameter descriptions. I'm doing this with mcp4k (in my case I'm using a lightweight compiler plugin).

This is not just useful for removing boiler-plate and readability, it also has the benefit of having other tooling (TypeDoc, IDE plugins etc.) work out of the box as they recognize and know JSDocs.

Looking at the bigger picture, this also helps LLMs write MCP applications. LLMs will be better at just writing "normal TS code" (which they are extremely good at) without having a set of MCP-specific rules to keep in mind.

I totally get the issue @jspahrsummers mentioned regarding JS compatibility though. Maybe this could be implemented for TS consumers while still allowing JS consumers doing it the "old way"? I'd love to help but my TS skills are limited.

@liudonghua123
Copy link

liudonghua123 commented Mar 24, 2025

Yeah, I would also like the decorator features of typescript. If the code is written in typescript, use decorator style would make the code clean, tidy and more readable.

For example, the following demo code in readme

// Simple tool with parameters
server.tool(
  "calculate-bmi",
  {
    weightKg: z.number(),
    heightM: z.number()
  },
  async ({ weightKg, heightM }) => ({
    content: [{
      type: "text",
      text: String(weightKg / (heightM * heightM))
    }]
  })
);

// Async tool with external API call
server.tool(
  "fetch-weather",
  { city: z.string() },
  async ({ city }) => {
    const response = await fetch(`https://api.weather.com/${city}`);
    const data = await response.text();
    return {
      content: [{ type: "text", text: data }]
    };
  }
);

Could rewrite it as the follows.

// Simple tool with parameters
@server.tool("calculate-bmi")
async function({ weightKg: number, heightM: number }) {
  return content: [{
    type: "text",
    text: String(weightKg / (heightM * heightM))
  }];
}

// Async tool with external API call
@server.tool("fetch-weather")
async function ({ city: str }) {
  const response = await fetch(`https://api.weather.com/${city}`);
  const data = await response.text();
  return {
    content: [{ type: "text", text: data }]
  };
}

And the decorator style code is more more like python.

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My App")


@mcp.tool()
def calculate_bmi(weight_kg: float, height_m: float) -> float:
    """Calculate BMI given weight in kg and height in meters"""
    return weight_kg / (height_m**2)


@mcp.tool()
async def fetch_weather(city: str) -> str:
    """Fetch current weather for a city"""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.weather.com/{city}")
        return response.text

See also https://github.com/tc39/proposal-decorators, https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators.

@jspahrsummers
Copy link
Member

@liudonghua123 Your example is not possible with decorators, as they are required to be applied to class methods, not free functions.

@liudonghua123
Copy link

liudonghua123 commented Mar 24, 2025

@liudonghua123 Your example is not possible with decorators, as they are required to be applied to class methods, not free functions.

yes, i read the related documentation and noticed this limitation, maybe this can implement in the future's proposal's.

It can only wrapped in a class or use some other strategies like higher order functions used in react currently.

@lloydzhou
Copy link

another way to define tool

by using ts-transformer-zod

interface BMIParam {
    iweightKg: number;
    heightM: string;
}

server.tool<BMIParam>(
  "calculate-bmi",
  async (args: BMIParam) => {
    const { weightKg, heightM } = args
    return {
      content: [{
        type: "text",
        text: String(weightKg / (heightM * heightM))
      }]
    }
  }
);
  1. install dependency
pnpm add -D ts-transformer-zod ts-patch

ts-patch install
  1. update tsconfig.json, and replace build command tsc to tspc
{
  "compilerOptions": {
    "plugins": [
      { "transform": "ts-transformer-zod/transformer" }
    ]
  }
}
  1. Overloading tool functions (this not impalement yet)

Support generic types

import { zobject } from "ts-transformer-zod";

tool<T>(
    name: string,
    description: string,
    cb: ToolCallback<T>,
): void {
    const paramsSchema = zobject<T>(); // just transformer to zod object
    // ....
}

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

No branches or pull requests

6 participants