Build a Code Snippet Vault MCP Server

Build an MCP server that gives AI assistants persistent code storage.

Introduction

30 Second Summary

Ever wish your AI assistant could remember that perfect code snippet you wrote last week? Right now, every conversation starts from scratch.

In this project, you will build a local MCP server that gives AI assistants persistent code storage powers. Your server will let any compatible AI client save, search, and retrieve code snippets stored on your machine.

What You'll Build

A TypeScript MCP server called "Snippet Vault" that exposes tools, resources, and prompts for managing a local collection of code snippets.

By the end of this project, you'll have:

  • A working MCP server with three tools for saving, searching, and deleting code snippets.
  • A JSON-based storage layer that persists snippets to disk across server restarts.
  • A tested server session using the MCP Inspector to verify all capabilities interactively.
  • Secret Mission: Add a favorites system with a toggle tool and a dedicated favorites resource.

Are there any prerequisites?

No prior experience with MCP is needed. This guide includes setup for all required tools, starting from scratch on a Windows device.

Set Up the Project

Before writing any server logic, you need a properly configured TypeScript project. The Model Context Protocol SDK uses ES modules and strict typing, so getting the project foundation right ensures everything compiles cleanly later.

In this step, you will create a new Node.js project, install the MCP SDK and its dependencies, and configure TypeScript for ES module output.

In this step, get ready to:
  • Create and initialize the snippet-vault project directory.
  • Install the MCP SDK, Zod, and TypeScript.
  • Configure TypeScript and package.json for ES module support.
Initialize the project

Every Node.js project starts with a dedicated folder and a package.json file that tracks dependencies and scripts.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir snippet-vault
cd snippet-vault
  • Confirm you are inside the snippet-vault folder by running:
ls

The folder is empty for now. That is expected.

  • Initialize a new npm project by running:
npm init -y

What does npm init -y do?

This command creates a package.json file with default values (project name, version 1.0.0, ISC license).

The -y flag accepts all defaults without prompting you for input.

You should see a confirmation that package.json was created.

  • Install the MCP SDK and Zod validation library by running:
npm install @modelcontextprotocol/sdk@1.29.0 zod@3.25.0
  • Install TypeScript and the Node.js type definitions as dev dependencies by running:
npm install --save-dev typescript@5.5.4 @types/node@22.12.0

What are these packages?

  • @modelcontextprotocol/sdk is the official TypeScript SDK for building MCP servers. It provides the server class, transport layers, and protocol handling.
  • zod is a schema validation library. The MCP SDK uses it to define and validate the input shapes for your tools and prompts.
  • typescript is the TypeScript compiler that converts your .ts files into JavaScript.
  • @types/node provides TypeScript type definitions for Node.js built-in modules like fs and path.
  • Verify the packages installed correctly by running:
ls node_modules

You should see a list of installed packages inside the node_modules folder.

Seeing an error during npm install?

  • Check your internet connection. npm downloads packages from a remote registry.
  • Make sure you are inside the snippet-vault folder. Run cd ~/Desktop/snippet-vault if you are unsure.
  • Confirm Node.js is installed by running node --version. You should see v24 in the output.

Help me troubleshoot my npm install error.

Configure TypeScript and package.json

TypeScript needs a configuration file to know how to compile your code. You also need to tell Node.js that this project uses ES modules instead of the older CommonJS format.

  • Create a new file called tsconfig.json in your snippet-vault folder and paste the following content:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

What do these settings do?

  • target: "ES2022" tells TypeScript to output modern JavaScript that Node.js 24 understands natively.
  • module: "Node16" and moduleResolution: "Node16" enable proper ES module support with .js extensions in imports.
  • outDir: "./build" puts compiled JavaScript files into a build folder, keeping source and output separate.
  • rootDir: "./src" tells the compiler that all your TypeScript source files live inside src/.
  • strict: true enables all strict type-checking options for safer code.
  • Save tsconfig.json.

Not sure how to create a file?

You can open any text editor (Notepad, VS Code, or your preferred editor), create a new file, paste the content above, and save it as tsconfig.json inside the snippet-vault folder.

Make sure the file is named exactly tsconfig.json (not tsconfig.json.txt).

  • Now open package.json (already in your snippet-vault folder from npm init) and replace its entire contents with:
{
  "name": "snippet-vault",
  "version": "1.0.0",
  "description": "An MCP server for storing and retrieving code snippets",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.29.0",
    "zod": "3.25.0"
  },
  "devDependencies": {
    "typescript": "5.5.4",
    "@types/node": "22.12.0"
  }
}

What changed in package.json?

  • "type": "module" tells Node.js to treat .js files as ES modules (using import/export) instead of the older CommonJS format (require/module.exports).
  • The build script runs the TypeScript compiler to convert your .ts source files into JavaScript.
  • The start script runs the compiled server from the build/ output folder.
  • Save package.json.
  • Confirm both config files exist by running:
ls

You should see node_modules, package.json, package-lock.json, and tsconfig.json listed in the output.

Missing tsconfig.json in the output?

  • Double-check that you saved the file inside the snippet-vault folder, not one level above it or on your Desktop.
  • Make sure the filename is exactly tsconfig.json with no extra extension like .txt.

Help me figure out why my config file is missing.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point.

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

package.json:

{
  "name": "snippet-vault",
  "version": "1.0.0",
  "description": "An MCP server for storing and retrieving code snippets",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.29.0",
    "zod": "3.25.0"
  },
  "devDependencies": {
    "typescript": "5.5.4",
    "@types/node": "22.12.0"
  }
}

Your project skeleton is ready. Next, you will build the storage layer that reads and writes code snippets to a local JSON file.

Build the Storage Layer

Your project is configured and ready to go. Now it's time to build the foundation that makes your MCP server actually useful: a storage layer that persists code snippets to disk.

The storage layer handles reading and writing snippets to a local JSON file. It provides functions that the MCP server will call whenever an AI assistant wants to save, find, or remove a snippet.

In this step, get ready to:
  • Create a storage module with a data model and file I/O helpers.
  • Implement CRUD functions for adding, searching, and deleting snippets.
  • Test the storage layer to verify snippets persist to disk.
Create the storage module

Every snippet your server stores needs a consistent shape and a reliable way to read and write to disk. This module defines what a snippet looks like and handles all file operations.

  • Create the src directory inside your snippet-vault folder by running this command:
mkdir src
  • Create a new file called storage.ts inside the src folder. This is where all your data logic will live.
  • Add the following code to src/storage.ts:
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

export interface Snippet {
  id: string;
  title: string;
  code: string;
  language: string;
  tags: string[];
  createdAt: string;
}

const __dirname = dirname(fileURLToPath(import.meta.url));
const STORAGE_PATH = join(__dirname, '..', 'snippets.json');

function loadSnippets(): Snippet[] {
  if (!existsSync(STORAGE_PATH)) {
    return [];
  }
  const data = readFileSync(STORAGE_PATH, 'utf-8');
  return JSON.parse(data);
}

function saveSnippets(snippets: Snippet[]): void {
  writeFileSync(STORAGE_PATH, JSON.stringify(snippets, null, 2), 'utf-8');
}

What does this code do?

The Snippet interface defines the shape of every snippet: an ID, title, actual code content, programming language, tags for categorization, and a creation timestamp.

The __dirname workaround is needed because ES modules don't have the built-in __dirname variable that older Node.js scripts use. STORAGE_PATH points to a snippets.json file one level up from the compiled output, which places it in your project root.

The loadSnippets() function checks if the file exists and returns an empty array if not. saveSnippets() writes the full array back to disk as formatted JSON.

You won't see a visible change yet. The next substep adds the functions that use this foundation.

Implement CRUD operations

With the file I/O helpers in place, you can now build the operations that create, read, and delete snippets. These are the functions your MCP server will eventually call.

  • Add the following functions below your saveSnippets function in src/storage.ts:
export function addSnippet(title: string, code: string, language: string, tags: string[]): Snippet {
  const snippets = loadSnippets();
  const snippet: Snippet = {
    id: crypto.randomUUID(),
    title,
    code,
    language,
    tags,
    createdAt: new Date().toISOString(),
  };
  snippets.push(snippet);
  saveSnippets(snippets);
  return snippet;
}

export function searchSnippets(query?: string, language?: string, tag?: string): Snippet[] {
  let snippets = loadSnippets();

  if (language) {
    snippets = snippets.filter(s => s.language.toLowerCase() === language.toLowerCase());
  }

  if (tag) {
    snippets = snippets.filter(s => s.tags.some(t => t.toLowerCase() === tag.toLowerCase()));
  }

  if (query) {
    const q = query.toLowerCase();
    snippets = snippets.filter(s =>
      s.title.toLowerCase().includes(q) ||
      s.code.toLowerCase().includes(q)
    );
  }

  return snippets;
}

export function getSnippetById(id: string): Snippet | undefined {
  const snippets = loadSnippets();
  return snippets.find(s => s.id === id);
}

export function deleteSnippet(id: string): boolean {
  const snippets = loadSnippets();
  const index = snippets.findIndex(s => s.id === id);
  if (index === -1) return false;
  snippets.splice(index, 1);
  saveSnippets(snippets);
  return true;
}

export function getAllSnippets(): Snippet[] {
  return loadSnippets();
}

What does this code do?

  • addSnippet() generates a unique ID with crypto.randomUUID(), builds a snippet object, appends it to the existing collection, and saves everything to disk.
  • searchSnippets() applies optional filters one at a time. It can filter by programming language, by tag, and by a keyword that matches in the title or code content. All comparisons are case-insensitive.
  • getSnippetById() looks up a single snippet by its unique ID.
  • deleteSnippet() finds a snippet by ID, removes it from the array, and saves the updated collection. It returns true on success or false if the ID was not found.
  • getAllSnippets() returns the entire collection for browsing.
  • Save src/storage.ts.

You won't see a runtime change yet. The next substep tests these functions to confirm everything works.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

export interface Snippet {
  id: string;
  title: string;
  code: string;
  language: string;
  tags: string[];
  createdAt: string;
}

const __dirname = dirname(fileURLToPath(import.meta.url));
const STORAGE_PATH = join(__dirname, '..', 'snippets.json');

function loadSnippets(): Snippet[] {
  if (!existsSync(STORAGE_PATH)) {
    return [];
  }
  const data = readFileSync(STORAGE_PATH, 'utf-8');
  return JSON.parse(data);
}

function saveSnippets(snippets: Snippet[]): void {
  writeFileSync(STORAGE_PATH, JSON.stringify(snippets, null, 2), 'utf-8');
}

export function addSnippet(title: string, code: string, language: string, tags: string[]): Snippet {
  const snippets = loadSnippets();
  const snippet: Snippet = {
    id: crypto.randomUUID(),
    title,
    code,
    language,
    tags,
    createdAt: new Date().toISOString(),
  };
  snippets.push(snippet);
  saveSnippets(snippets);
  return snippet;
}

export function searchSnippets(query?: string, language?: string, tag?: string): Snippet[] {
  let snippets = loadSnippets();

  if (language) {
    snippets = snippets.filter(s => s.language.toLowerCase() === language.toLowerCase());
  }

  if (tag) {
    snippets = snippets.filter(s => s.tags.some(t => t.toLowerCase() === tag.toLowerCase()));
  }

  if (query) {
    const q = query.toLowerCase();
    snippets = snippets.filter(s =>
      s.title.toLowerCase().includes(q) ||
      s.code.toLowerCase().includes(q)
    );
  }

  return snippets;
}

export function getSnippetById(id: string): Snippet | undefined {
  const snippets = loadSnippets();
  return snippets.find(s => s.id === id);
}

export function deleteSnippet(id: string): boolean {
  const snippets = loadSnippets();
  const index = snippets.findIndex(s => s.id === id);
  if (index === -1) return false;
  snippets.splice(index, 1);
  saveSnippets(snippets);
  return true;
}

export function getAllSnippets(): Snippet[] {
  return loadSnippets();
}
Test the storage manually

Before wiring the storage to the MCP server, you should verify it can actually create and persist a snippet on disk.

  • Add the following temporary test lines at the very bottom of src/storage.ts:
// Temporary test - remove after verifying
const testSnippet = addSnippet('Hello World', 'console.log("Hello!")', 'typescript', ['test']);
console.log('Created snippet:', testSnippet);

What does this test do?

This calls addSnippet() with sample data, which triggers the full save-to-disk flow. If everything works, snippets.json appears in your project root with the snippet inside it.

  • Save src/storage.ts.
  • Compile and run the storage module by running this command:
npx tsc && node build/storage.js
  • You should see output that looks like this:
Created snippet: {
  id: 'some-uuid-here',
  title: 'Hello World',
  code: 'console.log("Hello!")',
  language: 'typescript',
  tags: [ 'test' ],
  createdAt: '2026-07-02T...'
}
  • Confirm that snippets.json now exists in your project root by running:
ls snippets.json

You should see snippets.json listed in the output.

Don't see the file or getting errors?

  • If you see a compilation error, check that your test lines are at the very bottom of the file, after all function definitions.
  • If snippets.json doesn't appear, make sure your tsconfig.json has "outDir": "./build" and "rootDir": "./src". The storage path resolves relative to the compiled file in build/.
  • If you see Cannot find module, confirm your package.json has "type": "module" set.

Help me debug my storage module test

  • Now remove the temporary test lines from the bottom of src/storage.ts. Delete the comment line and the two lines below it.
  • Save src/storage.ts.

Why remove the test code?

The test lines run every time the file is loaded. Once the MCP server imports this module, you don't want it creating a test snippet on every startup. The functions remain available for the server to call on demand.

Your storage layer is complete. Next up, you'll create the MCP server itself and wire these functions into tools that an AI assistant can call.

Create the MCP Server with Tools

Your storage layer can save, search, and delete snippets. But right now, only your own code can call those functions.

To let AI assistants use your storage functions, you need to wrap them in an MCP server. The server exposes your functions as "tools" that any MCP-compatible client can discover and call over the JSON-RPC protocol.

In this step, get ready to:
  • Set up the MCP server entry point with imports and configuration.
  • Register three tools for saving, searching, and deleting snippets.
  • Connect the stdio transport and verify compilation.
Set up the server entry point

The McpServer class from the official SDK handles all the protocol negotiation for you. You just need to create an instance and tell it what capabilities to advertise.

  • Create a new file called src/index.ts in your snippet-vault folder.
  • Add the following imports and server initialization:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { addSnippet, searchSnippets, deleteSnippet, getAllSnippets } from './storage.js';

// Create the MCP server instance with a name and version
const server = new McpServer({
  name: 'snippet-vault',
  version: '1.0.0',
});

What does this code do?

The first two imports bring in the MCP SDK's server class and the stdio transport (the communication channel between your server and AI clients).

The z import is Zod, a schema validation library. The MCP SDK uses Zod schemas to define the input types for each tool.

Notice the imports use ./storage.js (not .ts). TypeScript with ES modules requires you to reference the compiled JavaScript output path in your imports.

  • Save src/index.ts.

You won't see a change yet. The next substep adds the tools that give this server its capabilities.

Register the three tools

MCP tools are functions that an AI assistant can call on demand. Each tool gets a name, a description (so the AI knows when to use it), and a typed input schema that validates arguments before your code runs.

  • In src/index.ts, add the save_snippet tool below the server instance:
server.registerTool(
  'save_snippet',
  {
    title: 'Save Snippet',
    description: 'Save a code snippet to the vault with a title, language, and tags',
    inputSchema: {
      title: z.string().describe('A descriptive title for the snippet'),
      code: z.string().describe('The code content'),
      language: z.string().describe('Programming language (e.g. typescript, python, rust)'),
      tags: z.array(z.string()).describe('Tags for categorization'),
    },
  },
  async ({ title, code, language, tags }) => {
    const snippet = addSnippet(title, code, language, tags);
    return {
      content: [
        {
          type: 'text' as const,
          text: `Snippet saved successfully!\nID: ${snippet.id}\nTitle: ${snippet.title}\nLanguage: ${snippet.language}\nTags: ${snippet.tags.join(', ')}`,
        },
      ],
    };
  }
);

What does this code do?

  • registerTool takes three arguments: the tool name, a config object (with title, description, and input schema), and a callback function.
  • The inputSchema uses Zod to define what inputs the tool accepts. The AI client uses these descriptions to figure out what arguments to pass.
  • The callback calls your addSnippet() function and returns a structured text response confirming the save.
  • Add the search_snippets tool below the save_snippet registration:
server.registerTool(
  'search_snippets',
  {
    title: 'Search Snippets',
    description: 'Search stored snippets by keyword, language, or tag',
    inputSchema: {
      query: z.string().optional().describe('Search keyword to match in title or code'),
      language: z.string().optional().describe('Filter by programming language'),
      tag: z.string().optional().describe('Filter by tag'),
    },
  },
  async ({ query, language, tag }) => {
    const results = searchSnippets(query, language, tag);
    if (results.length === 0) {
      return {
        content: [{ type: 'text' as const, text: 'No snippets found matching your criteria.' }],
      };
    }
    const formatted = results
      .map(s => `[${s.id}] ${s.title} (${s.language})\nTags: ${s.tags.join(', ')}\n---\n${s.code}`)
      .join('\n\n');
    return {
      content: [{ type: 'text' as const, text: `Found ${results.length} snippet(s):\n\n${formatted}` }],
    };
  }
);

What does this code do?

  • All three inputs use .optional(), meaning the AI can call this tool with any combination of filters or no filters at all.
  • The callback formats results into a readable text block showing each snippet's ID, title, language, tags, and code.
  • If no snippets match, it returns a friendly "not found" message instead of an empty response.
  • Add the delete_snippet tool below the search_snippets registration:
server.registerTool(
  'delete_snippet',
  {
    title: 'Delete Snippet',
    description: 'Delete a snippet from the vault by its ID',
    inputSchema: {
      id: z.string().describe('The ID of the snippet to delete'),
    },
  },
  async ({ id }) => {
    const success = deleteSnippet(id);
    return {
      content: [
        {
          type: 'text' as const,
          text: success
            ? `Snippet ${id} deleted successfully.`
            : `Snippet with ID ${id} not found.`,
        },
      ],
    };
  }
);

What does this code do?

  • This tool takes a single id input and passes it to your deleteSnippet() function.
  • It returns different messages depending on whether the snippet existed. This helps the AI give accurate feedback to the user.
  • Save src/index.ts.

The server can't run yet. The next substep adds the transport layer and verifies everything compiles.

Connect the stdio transport

An MCP server needs a transport to communicate with clients. The stdio transport reads JSON-RPC messages from standard input and writes responses to standard output. This is the simplest transport and works perfectly with the MCP Inspector.

Why use console.error instead of console.log?

In a stdio-based MCP server, stdout is reserved exclusively for JSON-RPC protocol messages between the server and client.

Any logging you add with console.log would corrupt the protocol stream. Use console.error instead, which writes to stderr and stays out of the way.

  • Add the main() function at the bottom of src/index.ts, below the last registerTool call:
// Start the server with stdio transport
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Snippet Vault MCP Server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error in main():', error);
  process.exit(1);
});

What does this code do?

  • StdioServerTransport creates the communication channel. server.connect(transport) wires it up and starts listening for incoming requests.
  • The .catch() at the bottom ensures any startup errors are logged and the process exits cleanly instead of hanging silently.
  • Save src/index.ts.
  • Compile the project by running this command:
npx tsc
  • The command should complete with no output (no errors). Confirm the compiled files exist by running:
ls build

You should see index.js and storage.js in the output.

Seeing TypeScript errors?

  • If you see "Cannot find module './storage.js'", make sure your import uses .js (not .ts). TypeScript ES modules require the compiled extension in import paths.
  • If you see errors about McpServer or StdioServerTransport not being found, double check that @modelcontextprotocol/sdk is installed. Run npm list to verify.
  • If you see type errors inside the tool callbacks, make sure you have type: 'text' as const (not just 'text') in each content object.

Help me debug my TypeScript compilation errors for the MCP server

✔️ Awesome, I've got everything!

Great. Make sure you've saved src/index.ts and confirmed the build/ directory contains both compiled files.

ⓧ I'd like to double check the full code

Here is the complete src/index.ts file. Compare it with yours to spot any differences.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { addSnippet, searchSnippets, deleteSnippet, getAllSnippets } from './storage.js';

const server = new McpServer({
  name: 'snippet-vault',
  version: '1.0.0',
});

server.registerTool(
  'save_snippet',
  {
    title: 'Save Snippet',
    description: 'Save a code snippet to the vault with a title, language, and tags',
    inputSchema: {
      title: z.string().describe('A descriptive title for the snippet'),
      code: z.string().describe('The code content'),
      language: z.string().describe('Programming language (e.g. typescript, python, rust)'),
      tags: z.array(z.string()).describe('Tags for categorization'),
    },
  },
  async ({ title, code, language, tags }) => {
    const snippet = addSnippet(title, code, language, tags);
    return {
      content: [
        {
          type: 'text' as const,
          text: `Snippet saved successfully!\nID: ${snippet.id}\nTitle: ${snippet.title}\nLanguage: ${snippet.language}\nTags: ${snippet.tags.join(', ')}`,
        },
      ],
    };
  }
);

server.registerTool(
  'search_snippets',
  {
    title: 'Search Snippets',
    description: 'Search stored snippets by keyword, language, or tag',
    inputSchema: {
      query: z.string().optional().describe('Search keyword to match in title or code'),
      language: z.string().optional().describe('Filter by programming language'),
      tag: z.string().optional().describe('Filter by tag'),
    },
  },
  async ({ query, language, tag }) => {
    const results = searchSnippets(query, language, tag);
    if (results.length === 0) {
      return {
        content: [{ type: 'text' as const, text: 'No snippets found matching your criteria.' }],
      };
    }
    const formatted = results
      .map(s => `[${s.id}] ${s.title} (${s.language})\nTags: ${s.tags.join(', ')}\n---\n${s.code}`)
      .join('\n\n');
    return {
      content: [{ type: 'text' as const, text: `Found ${results.length} snippet(s):\n\n${formatted}` }],
    };
  }
);

server.registerTool(
  'delete_snippet',
  {
    title: 'Delete Snippet',
    description: 'Delete a snippet from the vault by its ID',
    inputSchema: {
      id: z.string().describe('The ID of the snippet to delete'),
    },
  },
  async ({ id }) => {
    const success = deleteSnippet(id);
    return {
      content: [
        {
          type: 'text' as const,
          text: success
            ? `Snippet ${id} deleted successfully.`
            : `Snippet with ID ${id} not found.`,
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Snippet Vault MCP Server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error in main():', error);
  process.exit(1);
});

Your MCP server compiles and has three fully functional tools. Next up, you'll add a resource and a prompt to complete all three MCP primitives.

Add Resources and Prompts

Your server already has tools that let AI assistants take actions on your snippet vault. Now it's time to give them a way to passively read data and receive structured instructions.

In MCP, a resource exposes data for the AI to read (like a GET endpoint), and a prompt provides a reusable template that shapes how the AI approaches a task. Together with tools, these form the three core primitives of an MCP server.

In this step, get ready to:
  • Register a resource that exposes the full snippet collection as JSON.
  • Register a prompt template that helps the AI search the vault.
  • Compile the final server with all three MCP primitives.
Register a resource

A resource gives an AI client read-only access to data without needing to call a tool. Your resource will serve the entire snippet collection at a custom URI so any connected AI can browse saved snippets at will.

  • In src/index.ts, find the closing ); of the delete_snippet tool registration.
  • Add the following resource registration below it, above the async function main() line:
server.registerResource(
  'all-snippets',
  'snippets://all',
  {
    title: 'All Snippets',
    description: 'View all stored code snippets in the vault',
    mimeType: 'application/json',
  },
  async (uri) => {
    const snippets = getAllSnippets();
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(snippets, null, 2),
        },
      ],
    };
  }
);

What does this code do?

The first argument 'all-snippets' is the internal name the SDK uses to identify this resource. The second argument 'snippets://all' is the URI that AI clients use to request it.

The callback receives the parsed URI and returns a contents array. Each entry includes the URI and the text payload. Here you return all snippets serialized as pretty-printed JSON.

Why a resource instead of a tool?

Tools are for actions that change state or require parameters. Resources are for data the AI can read without any side effects. An AI client can proactively fetch a resource to build context before deciding which tool to call.

Register a prompt template

A prompt template gives the AI a reusable starting instruction for a specific task. Your prompt will accept a description of what the user is looking for and return a structured message that guides the AI to search the vault effectively.

  • Still in src/index.ts, add the following prompt registration directly below the resource you just added (and still above async function main()):
server.registerPrompt(
  'find-snippet',
  {
    title: 'Find a Snippet',
    description: 'Help me find a specific code snippet from my vault',
    argsSchema: {
      description: z.string().describe('Describe what kind of snippet you are looking for'),
    },
  },
  ({ description }) => ({
    messages: [
      {
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `I'm looking for a code snippet in my vault. Here's what I need: ${description}\n\nPlease search my snippet vault and help me find the most relevant snippet. If nothing matches exactly, suggest what tags or keywords I should search for.`,
        },
      },
    ],
  })
);

What does this code do?

The argsSchema defines the inputs the prompt expects. Here it takes a single description string from the user.

The callback returns a messages array that structures the conversation for the AI. The template interpolates the user's description into a helpful instruction that asks the AI to search the vault and offer suggestions.

  • Save src/index.ts.

Seeing red squiggly lines in your editor?

Make sure the resource and prompt blocks are placed between the last registerTool call and the async function main() line. If they're inside or after main(), TypeScript will report errors.

Also check that every opening parenthesis and brace has a matching closing one. The nested object structure makes it easy to miss a bracket.

Help me fix TypeScript errors in my MCP server resource and prompt registration.

Build the final server

Your server now has all three MCP primitives: tools, a resource, and a prompt. Time to compile and verify everything works together.

  • Compile the project by running this command in your terminal:
npx tsc
  • Verify the compilation succeeded with no errors in the terminal output.
  • Confirm the build/ directory contains index.js and storage.js by running:
ls build

You should see index.js and storage.js listed in the output.

Seeing compilation errors?

If TypeScript reports errors, check that you added the resource and prompt code between the last registerTool closing and the main() function. Also check the double-check tab below to compare your full file.

Help me fix my TypeScript compilation errors.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/index.ts file with all tools, the resource, and the prompt:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { addSnippet, searchSnippets, deleteSnippet, getAllSnippets } from './storage.js';

const server = new McpServer({
  name: 'snippet-vault',
  version: '1.0.0',
});

server.registerTool(
  'save_snippet',
  {
    title: 'Save Snippet',
    description: 'Save a code snippet to the vault with a title, language, and tags',
    inputSchema: {
      title: z.string().describe('A descriptive title for the snippet'),
      code: z.string().describe('The code content'),
      language: z.string().describe('Programming language (e.g. typescript, python, rust)'),
      tags: z.array(z.string()).describe('Tags for categorization'),
    },
  },
  async ({ title, code, language, tags }) => {
    const snippet = addSnippet(title, code, language, tags);
    return {
      content: [
        {
          type: 'text' as const,
          text: `Snippet saved successfully!\nID: ${snippet.id}\nTitle: ${snippet.title}\nLanguage: ${snippet.language}\nTags: ${snippet.tags.join(', ')}`,
        },
      ],
    };
  }
);

server.registerTool(
  'search_snippets',
  {
    title: 'Search Snippets',
    description: 'Search stored snippets by keyword, language, or tag',
    inputSchema: {
      query: z.string().optional().describe('Search keyword to match in title or code'),
      language: z.string().optional().describe('Filter by programming language'),
      tag: z.string().optional().describe('Filter by tag'),
    },
  },
  async ({ query, language, tag }) => {
    const results = searchSnippets(query, language, tag);
    if (results.length === 0) {
      return {
        content: [{ type: 'text' as const, text: 'No snippets found matching your criteria.' }],
      };
    }
    const formatted = results
      .map(s => `[${s.id}] ${s.title} (${s.language})\nTags: ${s.tags.join(', ')}\n---\n${s.code}`)
      .join('\n\n');
    return {
      content: [{ type: 'text' as const, text: `Found ${results.length} snippet(s):\n\n${formatted}` }],
    };
  }
);

server.registerTool(
  'delete_snippet',
  {
    title: 'Delete Snippet',
    description: 'Delete a snippet from the vault by its ID',
    inputSchema: {
      id: z.string().describe('The ID of the snippet to delete'),
    },
  },
  async ({ id }) => {
    const success = deleteSnippet(id);
    return {
      content: [
        {
          type: 'text' as const,
          text: success
            ? `Snippet ${id} deleted successfully.`
            : `Snippet with ID ${id} not found.`,
        },
      ],
    };
  }
);

server.registerResource(
  'all-snippets',
  'snippets://all',
  {
    title: 'All Snippets',
    description: 'View all stored code snippets in the vault',
    mimeType: 'application/json',
  },
  async (uri) => {
    const snippets = getAllSnippets();
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(snippets, null, 2),
        },
      ],
    };
  }
);

server.registerPrompt(
  'find-snippet',
  {
    title: 'Find a Snippet',
    description: 'Help me find a specific code snippet from my vault',
    argsSchema: {
      description: z.string().describe('Describe what kind of snippet you are looking for'),
    },
  },
  ({ description }) => ({
    messages: [
      {
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `I'm looking for a code snippet in my vault. Here's what I need: ${description}\n\nPlease search my snippet vault and help me find the most relevant snippet. If nothing matches exactly, suggest what tags or keywords I should search for.`,
        },
      },
    ],
  })
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Snippet Vault MCP Server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error in main():', error);
  process.exit(1);
});

Your server is now feature-complete with all three MCP primitives. Next up, you'll launch the MCP Inspector and test every capability interactively.

Test with the MCP Inspector

Your server is compiled and feature-complete with all three MCP primitives: tools, a resource, and a prompt.

Now you need to verify that everything actually works end-to-end. The MCP Inspector is a browser-based testing tool that connects to your server, discovers its capabilities, and lets you call each one interactively.

In this step, get ready to:
  • Launch the MCP Inspector and connect it to your server.
  • Test each tool, resource, and prompt interactively.
  • Verify that snippets persist on disk across server restarts.
Launch the MCP Inspector

The MCP Inspector runs directly through npx without any installation or account. It opens a browser-based UI where you can interact with your server's tools, resources, and prompts.

  • Launch the Inspector with your compiled server by running this command:
npx @modelcontextprotocol/inspector node build/index.js

What does this command do?

The npx @modelcontextprotocol/inspector part downloads and runs the Inspector tool. The node build/index.js part tells the Inspector which server to connect to.

The Inspector launches your server as a child process and communicates with it over stdio transport, the same way a real AI client would connect.

  • A browser window opens with the Inspector UI. You should see a connection established to your snippet-vault server.

Browser didn't open?

The terminal output should display a local URL. Copy and paste it into your browser manually. If you see a connection error, make sure npx tsc completed without errors in the previous step.

help me troubleshoot why the MCP Inspector isn't connecting to my server

Test each capability

The Inspector UI has separate tabs for each MCP primitive. You will walk through each one to confirm your server responds correctly.

Save a snippet with the save_snippet tool
  • Click the Tools tab in the Inspector.
  • Select save_snippet from the list of available tools.
  • Fill in the input fields with these test values:
  • title: Hello World
  • code: console.log('Hello, world!')
  • language: javascript
  • tags: ["basics", "greeting"]
  • Execute the tool and verify the response shows a generated ID, title, language, and tags.

What should I see?

The response should include text starting with "Snippet saved successfully!" followed by a UUID, the title "Hello World", the language "javascript", and tags "basics, greeting". Copy the ID value. You will need it later to test deletion.

Save a second snippet

Having two snippets makes the search test more meaningful.

  • With save_snippet still selected, fill in these values:
  • title: Fetch Data
  • code: const res = await fetch(url)
  • language: typescript
  • tags: ["http", "async"]
  • Execute the tool and confirm it saves successfully with a new ID.
Search snippets by language
  • Select search_snippets from the Tools tab.
  • Enter javascript in the language field. Leave the other fields empty.
  • Execute the tool.

You should see only the "Hello World" snippet in the results. The TypeScript snippet is filtered out because you searched by language.

Browse the resource
  • Click the Resources tab in the Inspector.
  • Click snippets://all to view the resource content.

You should see both snippets displayed as formatted JSON, including their IDs, titles, code, languages, and tags.

What is the difference between the resource and the search tool?

The snippets://all resource gives AI clients passive read-only access to the entire collection. The search_snippets tool gives the AI an active capability to filter and query. Resources provide context. Tools perform actions.

Preview the prompt
  • Click the Prompts tab in the Inspector.
  • Select find-snippet from the list.
  • Enter a function that makes HTTP requests in the description argument field.
  • Preview the generated message.

You should see a user message asking the AI to search the vault for code related to HTTP requests.

Delete a snippet
  • Switch back to the Tools tab.
  • Select delete_snippet from the list.
  • Paste the ID you copied from the first save_snippet call into the id field.
  • Execute the tool.

The response should confirm the snippet was deleted successfully.

Forgot to copy the ID?

Switch to the Resources tab, click snippets://all, and grab the ID from the JSON output. Then switch back to Tools to complete the deletion.

Verify persistence

Your storage layer writes to a JSON file on disk. Closing and relaunching the server should not lose any data.

  • Stop the Inspector by pressing Ctrl+C in your terminal.
  • Confirm the snippets.json file exists in your project root by running:
type snippets.json

You should see the remaining snippet ("Fetch Data") as formatted JSON. The deleted "Hello World" snippet should not appear.

  • Relaunch the Inspector to confirm data persists across restarts:
npx @modelcontextprotocol/inspector node build/index.js
  • Click the Resources tab and view snippets://all.

Your "Fetch Data" snippet should still be there. This confirms your storage layer persists data correctly across server restarts.

Snippets.json is empty or missing?

The file is created relative to the compiled build/ directory. Check that STORAGE_PATH in src/storage.ts uses join(__dirname, '..', 'snippets.json') which resolves to the project root. If you ran npx tsc after making changes, rebuild and retest.

help me debug why my snippets.json file is missing or empty

Secret mission

Add a Favorites System

Your snippet vault works great, but what if some snippets are more important than others? In this secret mission, you'll add a favorites system that lets an AI assistant mark snippets as favorites and browse them through a dedicated resource.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, pause by closing your terminal, or delete everything entirely.

Resources you used:

  • snippet-vault project folder on your Desktop (contains source code, build/ output, node_modules, and snippets.json).
  • Node.js v24 installation (system-level).

Keep everything running

No action needed. Choose this if you plan to connect your Snippet Vault server to an AI client like Claude Desktop or Cursor next.

  • Your MCP server only runs when you launch it. It does not consume resources in the background.
  • Your snippets.json file persists your saved snippets on disk for next time.
  • Node.js remains installed on your system for future projects.

Pause - I'll come back to this later

Close any open terminals to free up system memory. Your project files stay on disk and are ready when you return.

  • Close the terminal window you used for the MCP Inspector.
  • Your snippet-vault folder and all its contents remain on your Desktop.
  • To resume later, open a terminal in the snippet-vault folder and run npx @modelcontextprotocol/inspector node build/index.js to relaunch the Inspector.

Delete - I don't want to use this again

Remove all project files and optionally uninstall Node.js from your system.

  • Close any terminal windows that are open in the snippet-vault directory.
  • Delete the project folder by opening a new terminal and running the command below for your system.

macOS

rm -rf ~/Desktop/snippet-vault

Windows

Remove-Item -Recurse -Force "$HOME\Desktop\snippet-vault"
  • To uninstall Node.js on Windows:
  • To uninstall Node.js on macOS, delete the Node.js binary and related folders:
sudo rm -rf /usr/local/bin/node /usr/local/lib/node_modules /usr/local/include/node
  • Confirm the project folder is gone by checking your Desktop. You should no longer see a snippet-vault folder.

Nice Work!

Nice Work!

You just built a fully functional MCP server from scratch in TypeScript. Any MCP-compatible AI client can now connect to your Snippet Vault and use it as persistent local code storage.

You learned how to:

  • Built a complete MCP server using the official @modelcontextprotocol/sdk, implementing all three MCP primitives: tools (save, search, delete), a resource (snippet collection), and a prompt (find snippet).
  • Created a persistent JSON storage layer that survives server restarts, giving your AI assistant a durable local memory.
  • Tested your server interactively using the MCP Inspector, verifying each tool, resource, and prompt works end to end.
  • Secret Mission: Added a favorites system with a toggle tool and a dedicated favorites resource.

Ready to quiz yourself?