MCP Server Using TypeScript and GitHub Models

From Logic Wiki
Jump to: navigation, search


Using the MCP TypeScript SDK to build an MCP server that integrates with GitHub Models. The server exposes resources and tools that list available models and compare their features and responses to prompts.

Github : https://github.com/LinkedInLearning/model-context-protocol-mcp-hands-on-with-agentic-ai-2034200/

Requirements

  • A Claude.ai account (MCP support is available for all account types)
  • The Claude Desktop app, available for macOS and Windows
  • Node.js (version 18 or higher)
  • A code editor like Visual Studio Code
  • A GitHub account with access to GitHub Models

Setting up the project

In terminal, navigate to the gh-models-helper folder:

cd gh-models-helper

Install the required dependencies defined in package.json:

npm install

Use .env.template to create a .env file in the project root and add your GitHub Personal Access Token:

GITHUB_TOKEN=your_github_token_here

TypeScript MCP server anatomy

The code in a TypeScript MCP server follows a four-step pattern:

  1. Import the MCP SDK and other dependencies
  2. Create a new MCP server instance
  3. Define resources, tools, and prompts
  4. Set up a transport for the server to communicate with clients

Here's the basic scaffolding, pared down to just the basics, as provided in the TypeScript SDK documentation:

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create an MCP server
const server = new McpServer({
  name: "Demo",
  version: "1.0.0"
});

// Add an addition tool
server.tool("add",
  { a: z.number(), b: z.number() },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }]
  })
);

// Add a dynamic greeting resource
server.resource(
  "greeting",
  new ResourceTemplate("greeting://{name}", { list: undefined }),
  async (uri, { name }) => ({
    contents: [{
      uri: uri.href,
      text: `Hello, ${name}!`
    }]
  })
);

// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);

Developing an MCP server with TypeScript

In src/index.ts you'll find the pre-built scaffolding and some helper functions for the MCP server. In the rest of this tutorial you'll add a resource, a tool, and a prompt to the file to build out the features of the server.

Register the models://available resource

The models://available resource exposes a list of available language models that can be used by the MCP client. To make the code example less lengthy, the actual API request to get the list is handled by the already defined getModelsData() helper function.

Here's how the resource is registered:

// Register the models://available resource
server.resource(
  "models://available",
  new ResourceTemplate("models://available", { list: undefined }),
  { description: "List available language models" },
  async (uri: ResourceUri) => {
    const models = await getModelsData();
    const formattedText = [
      "# Available Models",
      "",
      ...models.map(model => [
        `## ${model.displayName} (\`${model.id}\`)`,
        `- Publisher: ${model.publisher}`,
        `- Context Window: ${model.context_window.toLocaleString()} tokens`,
        `- Summary: ${model.summary}`,
        ""
      ]).flat()
    ].join("\n");

    return {
      contents: [{
        uri: uri.href,
        text: formattedText
      }]
    };
  }
);

Note the description property. In TypeScript, this is how the client LLM identifies when and how to access the resource or tool. The description can be short as in this example, or more expansive if the feature is more complex.

This resource uses the ResourceTemplate class to define the resource structure, and formats the model data into a readable markdown format. As explained in the Python tutorial, this formatting is not required as the LLM will be able to format the response to fit the user request, but if you want to control what the LLM has access to, this is how you do it.

Register the compare_models tool

The compare_models tool allows the LLM to respond from different models for the same prompt by sending completion requests to the GitHub Models API. Again, to make the code example less lengthy, the actual API request is handled by a helper function, this time compareModels().

// Register the compare_models tool
server.tool(
  "compare_models",
  "Compare responses from different models for the same prompt",
  {
    prompt: z.string()
      .describe("The prompt to send to all models"),
    models: z.array(z.string())
      .default(["gpt-4", "claude-3"])
      .describe("List of model IDs to compare")
  },
  async ({ prompt, models }: CompareModelsParams) => {
    const availableModels = await getModelsData();
    const validModels = models.filter((id: string) => 
      availableModels.some(model => model.id === id)
    );

    if (validModels.length === 0) {
      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            error: "No valid models specified",
            available_models: availableModels.map(m => m.id)
          }, null, 2)
        }]
      };
    }

    const responses = await compareModels(prompt, validModels);
    
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          results: responses,
          summary: {
            models_compared: validModels,
            prompt
          }
        }, null, 2)
      }]
    };
  }
);

For this tool there is no explicit description property defined but the LLM description is still present as a string. Directly after you find the prompt and models arguments for the tool, each with a natural language descriptor providing the LLM with necessary context to supply the correct information:

{
    prompt: z.string()
        .describe("The prompt to send to all models"),
    models: z.array(z.string())
        .default(["gpt-4", "claude-3"])
        .describe("List of model IDs to compare")
}

Finally the tool returns the raw completions as JSON-formatted data that can be processed by the LLM.

Register the compare_models_prompt

The compare_models_prompt exposes a prompt template for the MCP client to create prompts for model comparison. Whether this feature is used or not depends on how the LLM interprets the user prompt.

// Register the compare_models_prompt
server.prompt(
  "compare_models_prompt",
  "Create a comparison prompt with selected models",
  {
    prompt: z.string()
      .describe("The prompt to compare"),
    models: z.string()
      .optional()
      .describe("Comma-separated list of models to compare")
  },
  ({ prompt, models }: CompareModelsPromptParams) => {
    const message = models
      ? `Please compare how different AI models respond to this prompt:\n\nPrompt: ${prompt}\n\nPlease use these specific models: ${models}`
      : `Please compare how different AI models respond to this prompt:\n\nPrompt: ${prompt}\n\nPlease use the default models available.`;

    return {
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: message
        }
      }]
    };
  }
);

This prompt receives a required prompt text derived by the LLM based on the user's prompt, and an optional model list for the models to be tested. The input is formatted as a structured message for model comparison, and as this example shows, a single prompt can have multiple different outputs depending on conditions.

The returned message from the prompt is formatted to be sent directly to the LLM, meaning the user may never see the actual returned message but rather the LLM's inference from the message.

Testing the MCP server

Once you've added the resource, tool, and prompt, it's time to test the server using the MCP Inspector. To do so, follow these steps:

  • Build the server in the /dist folder:
npm run build
  • Start your MCP server in developer mode:
npx @modelcontextprotocol/inspector node dist/index.js
  • The MCP Inspector will be available at http://localhost:5173 - open this URL in your browser
  • In the Inspector:
    • Click "Connect" to establish a connection to your server
    • Use the "Resources" tab to explore available models
    • Use the "Tools" tab to test the list_available_models tool
    • Use the "Prompts" tab to try out the compare_models_prompt

Run the MCP server in Claude Desktop

This MCP server exposes requests to GitHub Models which requires authentication. During development the authentication is handled by the .env file. When you want to run the server in Claude Desktop for testing purposes, you can either set the MCP server up in a Docker container with environmental variables set, or you can provide the environment variable directly in the server object in claude_desktop_config.json like this:

{
    "mcpServers": {
        "GitHub Models Comparison": {
            "command": "node",
            "args": [
                "/absolute/path/to/gh-models-helper/dist/index.js"
            ],
            "env": {
                "GITHUB_TOKEN": "your_github_personal_access_token"
            }
        }
    }
}

NOTE: This is not a secure way of handling keys. If you intend to publish your MCP server, or use it in an insecure environment, set up proper authentication mechanisms, add validation for client credentials, handle tokens securely, and use proper security practices.

Links