Podcast Title

Author Name

0:00
0:00
Album Art

Building a Custom MCP Server with TypeScript Explained in 10 Minutes

By 10xdev team August 17, 2025

In this article, we'll explore building a custom MCP (Model Context Protocol) server using TypeScript. The goal is to create a system that provides a Large Language Model (LLM) with additional, external information, enhancing its decision-making capabilities.

The Core Problem: LLMs Lack External Context

Let's consider a common scenario. You're working on a project within a team. Important discussions, decisions, and documentation are spread across various systems like Google Docs, Confluence, or Notion. When you're using an LLM-powered tool, like an AI code editor, it lacks access to this external knowledge.

For instance, if you ask the LLM to start working on a task based on a recent team agreement, it can only analyze the existing codebase.

Example Query:

"We had an agreement yesterday about reorganizing the backend system. I'd like to start working on it. Please check our last decision and let's begin."

The expected result is that the LLM will search the project files and find nothing, because the information resides elsewhere. This is the problem we aim to solve.

A Better Approach: A Unified MCP Server with Plugins

The most straightforward approach might be to enable different MCPs for each system—Confluence, Notion, Jira, Google Docs, etc.—and have the LLM trigger them one by one. However, this can be inefficient and costly, as you might pay for every API call to each MCP server.

A more elegant solution is to create a single, unified MCP server that acts as a central knowledge hub.

Our proposed system: - A Central MCP Server: A generic, "no-flow" MCP that knows how to communicate with various external systems. - A Plugin Architecture: For each external system (Jira, Confluence, Slack, GitHub, Google Docs), we can create a plugin. Adding a new knowledge source to your organization becomes as simple as adding a new plugin. - Intelligent Information Retrieval: The server can fetch information from multiple sources simultaneously. For example, it could retrieve transcripts from Google Meetings stored in Google Docs, giving the LLM context from recent discussions. - Smarter Requests: We can allow the LLM to generate a request that specifies a potential system. For example, if the query mentions a ticket number, the LLM might guess the system is Jira. Our server can then prioritize searching Jira while still checking other sources.

This is the core idea we will start implementing in this article.

Getting Started: Building the Server with TypeScript

For this project, we are using TypeScript. While Python is excellent for data engineering and machine learning tasks, TypeScript and Node.js are common choices for modern backend development.

First, we need to add the necessary dependencies: - TypeScript SDK: For building the MCP server. - Zod: A library for schema declaration and validation, which we'll use for generating comments for our parameters and validating the LLM's input.

The Entry Point: index.ts

Our starting point is index.ts. The setup is quite straightforward.

  • We import stdioServerTransport, which allows the MCP server to run locally and communicate via standard input/output streams. This is the simplest method for local development.
  • The alternative is a remote server using the SSE (Server-Sent Events) protocol, which we are not using at this time.
  • We connect to our server definition and print a confirmation message once it's up and running.

Here is the code: ```typescript import { stdioServerTransport } from "@mcp/core"; import { server } from "./server";

// Connect to the server using standard I/O const transport = stdioServerTransport(); server.connect(transport);

console.error("No-flow MCP server running on stdio"); ```

The Server Logic: server.ts

Next, we define the server itself. We are starting with a skeleton application that doesn't have any real plugins yet. The goal is to construct something we can run and verify that the overall approach works.

In server.ts, we define a single tool named searchKnowledge.

  • Function Definition: The LLM uses the function name (searchKnowledge) and its description to understand the tool's capabilities and when to use it.
  • Parameters: We define three parameters:
    1. query: The search query to find relevant information.
    2. system: The preferred knowledge system (e.g., Jira, Confluence). This is optional, but if specified, results from this system will be prioritized.
    3. limit: The maximum number of results to return, defaulting to three.
  • Method Body: The execute method contains the logic. For now, it simply logs the received query and returns a dummy response.

Note: When communicating via standard I/O, you cannot use console.log for debugging as it would interfere with the data stream. We use console.error for logging instead.

Here is the initial implementation: ```typescript import { MCPServer, z } from "@mcp/core";

export const server = new MCPServer({ tools: { searchKnowledge: { description: "Searches the knowledge base for a query.", parameters: z.object({ query: z.string().describe("Search query to find relevant information"), system: z.string().optional().describe("Preferred knowledge system or source to search in. If specified, results from this system will be prioritized."), limit: z.number().default(3).describe("Max number of results to return"), }), execute: async ({ query, system, limit }) => { console.error(Received search query: ${query}, system: ${system}, limit: ${limit});

    // Dummy response for initial testing
    const dummyResponse = {
      content: "Key decisions were made: 1. Adopt a monorepo structure using NX. 2. Implement the backend in Rust.",
      metadata: {
        source: "architecture-notes.md",
        system: "docs",
        lastModified: "2025-08-16",
        confidence: 0.9,
      },
    };

    return JSON.stringify(dummyResponse, null, 2);
  },
},

}, }); ```

Testing with the MCP Inspector

Before integrating the server with a real system like Cursor, it's wise to test it. The mcp-inspector tool is perfect for this. It provides a web interface to interact with your local MCP server.

You can run the inspector with the following command: bash npx @mcp/inspector --cmd "node index.js" This command starts the inspector and provides a local URL. In the web UI, you can connect to the server and see the console logs. You can list the available tools and execute them directly. When you execute the searchKnowledge tool with a dummy query, you'll see the tool call and the hardcoded JSON response we created. This confirms the basic setup is working correctly.

Integrating with a Real System and Identifying a Problem

Now, let's integrate this with an AI code editor. In the editor's settings, we add a new MCP and point it to our server's execution command. The editor will automatically read the tool's definition, including the function name and parameter descriptions.

Let's try our original query again:

"We had an agreement yesterday about reorganizing the backend system. I'd like to start working on it. Please check our last decision and let's begin."

The LLM correctly identifies the intent and calls our searchKnowledge tool. It receives our dummy response and presents the information.

However, there's a problem. The response is missing crucial context. - Where did the information come from? - When was it last modified? - Was the confidence score used?

The LLM simply extracts the content field and ignores the valuable metadata. This happens because, unlike with tool parameters, the TypeScript SDK doesn't currently have a structured way to describe the output format to the LLM.

The Solution: Enhancing the Function Definition

To fix this, we need to give the LLM more instructions on how to interpret the response. We can do this by expanding the tool's main description field. We'll provide explicit instructions on how to handle each field in the JSON response and even specify the available knowledge systems.

Here is the updated server.ts with a much more detailed description:

import { MCPServer, z } from "@mcp/core";

export const server = new MCPServer({
  tools: {
    searchKnowledge: {
      description: `Searches the knowledge base for a query.
Available knowledge systems: jira, confluence, slack, docs.

Usage Instructions:
- The 'content' field contains the information found.
- The 'lastModified' field indicates the date the information was last updated. Always mention this date in your response.
- The 'confidence' field is a score from 0.0 to 1.0. Use it to weigh the relevance of the information.
- The 'source' field indicates the origin of the data (e.g., a file name or URL). Cite this source in your response.

Response Format:
Your response should be a summary of the findings, citing the source and modification date for each piece of information.`,
      parameters: z.object({
        query: z.string().describe("Search query to find relevant information"),
        system: z.string().optional().describe("Preferred knowledge system (e.g., jira, confluence). If specified, results from this system will be prioritized."),
        limit: z.number().default(3).describe("Max number of results to return"),
      }),
      execute: async ({ query, system, limit }) => {
        console.error(`Received search query: ${query}, system: ${system}, limit: ${limit}`);

        const dummyResponse = {
          content: "Key decisions were made: 1. Adopt a monorepo structure using NX. 2. Implement the backend in Rust.",
          metadata: {
            source: "architecture-notes.md",
            system: "docs",
            lastModified: "2025-08-16",
            confidence: 0.9,
          },
          instructions: "Cite the source and lastModified date in your response."
        };

        return JSON.stringify(dummyResponse, null, 2);
      },
    },
  },
});

After refreshing the server in the editor's settings, the LLM now has a comprehensive guide on how to process the output.

Final Test and Next Steps

Let's try the same query one last time. Now, the LLM correctly identifies that the information likely comes from "docs" and formulates a better response.

Improved LLM Response:

"According to the document architecture-notes.md, which was last modified on 2025-08-16, several agreements were made. The two key decisions were to adopt a monorepo structure using NX and to implement the backend in Rust."

This is exactly what we wanted. The response is now contextual and actionable.

Furthermore, if we provide a more specific query, the LLM is now clever enough to use the system parameter correctly.

Example Query:

"I'm working on Jira ticket PROJ-5342. Can you find the discussion on how we should proceed?"

The LLM will now make a tool call with query: "PROJ-5342 discussion" and system: "jira".

This article covered the first step in building a truly valuable MCP server. We've established a working skeleton and a method for ensuring the LLM provides rich, contextual responses. In future sessions, we will implement the plugins for real systems to progress toward our final goal.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Recommended For You

Up Next