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

Simplified, Express-like API #117

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft

Conversation

jspahrsummers
Copy link
Member

@jspahrsummers jspahrsummers commented Jan 7, 2025

Inspired by #116 and some of the MCP SDK wrappers that have popped up in the ecosystem, this is an attempt to bring a more Express-style API into the SDK.

This diverges from the existing wrapper libraries through liberal use of overloads to achieve a highly ergonomic API supporting a variable number of parameters.

Zod (already used in the SDK) is further utilized to declare tool input shapes, and perform automatic parsing and validation. I tried to limit its syntactic overhead, though.

Examples

Tools

server.tool("save", async () => {
  return {
    content: [
      {
        type: "text",
        text: "Saved successfully.",
      },
    ],
  };
});

server.tool("echo", { text: z.string() }, async ({ text }) => {
  return {
    content: [
      {
        type: "text",
        text,
      },
    ],
  };
});

server.tool(
  "add",
  "Adds two numbers together",
  { a: z.number(), b: z.number() },
  async ({ a, b }) => {
    return {
      content: [
        {
          type: "text",
          text: String(a + b),
        },
      ],
    };
  },
);

Resources

// 1. Basic static resource with fixed URI
server.resource(
  "welcome-message",
  "test://messages/welcome", 
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: "Welcome to the server!"
    }]
  })
);

// 2. Static resource with metadata
server.resource(
  "documentation",
  "test://docs/index",
  {
    description: "Server documentation",
    mimeType: "text/markdown"
  },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: "# Server Documentation\n\nThis is the main documentation page."
    }]
  })
);

// 3. Resource template for dynamic URIs
server.resource(
  "user-profile",
  new ResourceTemplate("test://users/{userId}/profile", { list: undefined }),
  async (uri, { userId }) => {
    return {
      contents: [{
        uri: uri.href,
        text: `Profile for user ${userId}`
      }]
    };
  }
);

// 4. Resource template with metadata
server.resource(
  "user-avatar",
  new ResourceTemplate("test://users/{userId}/avatar", { list: undefined }),
  {
    description: "User avatar image",
    mimeType: "image/png"
  },
  async (uri) => {
    // Example image data
    const imageData = new Uint8Array([0xFF, 0xD8, 0xFF]); // Example JPEG header

    return {
      contents: [{
        uri: uri.href,
        blob: Buffer.from(imageData).toString("base64")
      }]
    };
  }
);

// 5. Resource template with list capability
server.resource(
  "chat-messages",
  new ResourceTemplate(
    "test://chats/{chatId}/messages/{messageId}",
    {
      list: async () => ({
        resources: [
          {
            name: "Message 1",
            uri: "test://chats/123/messages/1"
          },
          {
            name: "Message 2", 
            uri: "test://chats/123/messages/2"
          }
        ]
      })
    }
  ),
  async (uri, { messageId, chatId }) => ({
    contents: [{
      uri: uri.href,
      text: `Message ${messageId} in chat ${chatId}`
    }]
  })
);

// 6. Resource template with metadata and list capability
server.resource(
  "photo-album",
  new ResourceTemplate(
    "test://albums/{albumId}/photos/{photoId}",
    {
      list: async () => ({
        resources: [
          {
            name: "Beach Photo",
            uri: "test://albums/vacation/photos/1",
            description: "Day at the beach"
          },
          {
            name: "Mountain Photo",
            uri: "test://albums/vacation/photos/2",
            description: "Mountain hiking"
          }
        ]
      })
    }
  ),
  {
    description: "Photo album contents",
    mimeType: "image/jpeg"
  },
  async (uri, { albumId, photoId }) => ({
    contents: [
      {
        uri: uri.href,
        blob: Buffer.from(/* photo data */).toString('base64')
      },
      {
        uri: `${uri.href}#metadata`,
        text: JSON.stringify({
          timestamp: new Date().toISOString(),
          location: "Beach",
          description: "Beautiful day at the beach"
        })
      }
    ]
  })
);

Prompts

// Basic prompt with no arguments
server.prompt("greeting", () => ({
  messages: [
    {
      role: "assistant",
      content: { type: "text", text: "Hello! How can I help you today?" }
    }
  ]
}));

// Prompt with description
server.prompt(
  "introduction",
  "A friendly introduction message for new users",
  () => ({
    messages: [
      {
        role: "assistant", 
        content: { 
          type: "text",
          text: "Welcome! I'm an AI assistant ready to help you with your tasks."
        }
      }
    ]
  })
);

// Prompt with arguments schema
server.prompt(
  "personalizedGreeting",
  {
    name: z.string(),
    language: z.string().optional()
  },
  ({ name, language }) => ({
    messages: [
      {
        role: "assistant",
        content: {
          type: "text", 
          text: `${language === "es" ? "¡Hola" : "Hello"} ${name}! How may I assist you today?`
        }
      }
    ]
  })
);

// Prompt with description and arguments schema
server.prompt(
  "customWelcome",
  "A customizable welcome message with user's name and preferred language",
  {
    name: z.string().describe("User's name"),
    language: z.enum(["en", "es", "fr"]).describe("Preferred language code"),
    formal: z.boolean().optional().describe("Whether to use formal language")
  },
  ({ name, language, formal = false }) => {
    let greeting;
    switch(language) {
      case "es":
        greeting = formal ? "Buenos días" : "¡Hola";
        break;
      case "fr":
        greeting = formal ? "Bonjour" : "Salut";
        break;
      default:
        greeting = formal ? "Good day" : "Hi";
    }

    return {
      description: "Personalized welcome message",
      messages: [
        {
          role: "assistant",
          content: {
            type: "text",
            text: `${greeting} ${name}!`
          }
        }
      ]
    };
  }
);

// Prompt with multiple messages in response
server.prompt(
  "conversation",
  {
    topic: z.string()
  },
  ({ topic }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Let's talk about ${topic}`
        }
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: `I'd be happy to discuss ${topic} with you. What would you like to know?`
        }
      }
    ]
  })
);

Alternatives considered

Decorators, although still not fully standardized in ECMAScript, are at stage 3 and already supported by TypeScript. I believe it would be possible to use them to craft some nice APIs that could be used like so:

class MyServer extends Server {
  @tool
  myCustomTool(someArg: number) {
    return "foobar";
  }
}

However, decorators are too foreign to many JS developers, and unfortunately are not supported on free functions—they can only annotate class members—so would not achieve an idiomatic API quite like FastMCP in the Python SDK.

A decorator-based API might be best as a community extension to the base SDK, which needs to be accessible to as wide an audience as possible.

To do

  • Resource templates
  • Move resource template listing into custom type
  • Prompts
  • Completion
  • Factor out into another class that wraps Server?

src/shared/uriTemplate.ts Dismissed Show dismissed Hide dismissed
src/shared/uriTemplate.ts Dismissed Show dismissed Hide dismissed
@jspahrsummers
Copy link
Member Author

jspahrsummers commented Jan 10, 2025

Ready for early review. This needs README updates and probably some other niceties, but would love to get any feedback on the APIs as defined and implemented.

Note that most of the added lines are tests.

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