← Back to Blog
tutorialtestingquality

Testing MCP Tools: Unit Tests, Integration Tests, and Mocking

Comprehensive testing strategies for MCP servers — unit test your tools, integration test with real clients.

·4 min read·xapable

Testing MCP Tools: Unit Tests, Integration Tests, and Mocking

MCP tools power AI agents. Bugs can cascade into bad agent decisions. This guide covers testing at every level.

Why Test MCP Tools?

  • Agents depend on correct tool behavior — wrong output = wrong agent actions
  • Schema changes break clients — test your inputSchema
  • Error handling matters — agents need clear error messages
  • Regressions are invisible — without tests, you won't know

Level 1: Unit Testing Tool Handlers

Extract tool logic into testable functions:

<span class="hljs-comment">// tools.js — pure functions, easy to test</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">addTodo</span>(<span class="hljs-params">todos, task, priority = <span class="hljs-string">&quot;medium&quot;</span></span>) {
  <span class="hljs-keyword">const</span> todo = {
    <span class="hljs-attr">id</span>: todos.<span class="hljs-property">length</span> + <span class="hljs-number">1</span>,
    task,
    priority,
    <span class="hljs-attr">created</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>().<span class="hljs-title function_">toISOString</span>()
  };
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">todos</span>: [...todos, todo],
    <span class="hljs-attr">message</span>: <span class="hljs-string">`✅ Added: &quot;<span class="hljs-subst">${task}</span>&quot;`</span>
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">listTodos</span>(<span class="hljs-params">todos, filter = <span class="hljs-string">&quot;all&quot;</span></span>) {
  <span class="hljs-keyword">const</span> filtered = filter === <span class="hljs-string">&quot;all&quot;</span>
    ? todos
    : todos.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> t.<span class="hljs-property">priority</span> === filter);
  <span class="hljs-keyword">return</span> filtered.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> <span class="hljs-string">`[<span class="hljs-subst">${t.priority.toUpperCase()}</span>] <span class="hljs-subst">${t.task}</span>`</span>);
}

Test with your preferred framework:

<span class="hljs-comment">// tools.test.js</span>
<span class="hljs-keyword">import</span> { describe, it, expect } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;vitest&quot;</span>;
<span class="hljs-keyword">import</span> { addTodo, listTodos } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;./tools.js&quot;</span>;

<span class="hljs-title function_">describe</span>(<span class="hljs-string">&quot;addTodo&quot;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;adds a todo with default priority&quot;</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> { todos, message } = <span class="hljs-title function_">addTodo</span>([], <span class="hljs-string">&quot;Buy milk&quot;</span>);
    <span class="hljs-title function_">expect</span>(todos).<span class="hljs-title function_">toHaveLength</span>(<span class="hljs-number">1</span>);
    <span class="hljs-title function_">expect</span>(todos[<span class="hljs-number">0</span>].<span class="hljs-property">task</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">&quot;Buy milk&quot;</span>);
    <span class="hljs-title function_">expect</span>(todos[<span class="hljs-number">0</span>].<span class="hljs-property">priority</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">&quot;medium&quot;</span>);
    <span class="hljs-title function_">expect</span>(message).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;Buy milk&quot;</span>);
  });

  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;adds a todo with custom priority&quot;</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> { todos } = <span class="hljs-title function_">addTodo</span>([], <span class="hljs-string">&quot;Fix bug&quot;</span>, <span class="hljs-string">&quot;high&quot;</span>);
    <span class="hljs-title function_">expect</span>(todos[<span class="hljs-number">0</span>].<span class="hljs-property">priority</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">&quot;high&quot;</span>);
  });
});

<span class="hljs-title function_">describe</span>(<span class="hljs-string">&quot;listTodos&quot;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;filters by priority&quot;</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> todos = [
      { <span class="hljs-attr">task</span>: <span class="hljs-string">&quot;A&quot;</span>, <span class="hljs-attr">priority</span>: <span class="hljs-string">&quot;high&quot;</span> },
      { <span class="hljs-attr">task</span>: <span class="hljs-string">&quot;B&quot;</span>, <span class="hljs-attr">priority</span>: <span class="hljs-string">&quot;low&quot;</span> }
    ];
    <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">listTodos</span>(todos, <span class="hljs-string">&quot;high&quot;</span>);
    <span class="hljs-title function_">expect</span>(result).<span class="hljs-title function_">toHaveLength</span>(<span class="hljs-number">1</span>);
    <span class="hljs-title function_">expect</span>(result[<span class="hljs-number">0</span>]).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;A&quot;</span>);
  });

  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;returns empty for no matches&quot;</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-title function_">expect</span>(<span class="hljs-title function_">listTodos</span>([], <span class="hljs-string">&quot;high&quot;</span>)).<span class="hljs-title function_">toEqual</span>([]);
  });
});

Level 2: Testing the MCP Protocol

Test the request/response cycle:

<span class="hljs-comment">// server.test.js</span>
<span class="hljs-keyword">import</span> { describe, it, expect } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;vitest&quot;</span>;

<span class="hljs-comment">// Simulate the tools/list handler</span>
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">simulateListTools</span>(<span class="hljs-params">server</span>) {
  <span class="hljs-comment">// You&#x27;d need to expose the handler for testing</span>
  <span class="hljs-comment">// or use the MCP SDK&#x27;s test utilities</span>
}

<span class="hljs-title function_">describe</span>(<span class="hljs-string">&quot;tools/list&quot;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;returns correct tool schema&quot;</span>, <span class="hljs-title function_">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> server.<span class="hljs-title function_">handleRequest</span>({
      <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;tools/list&quot;</span>,
      <span class="hljs-attr">params</span>: {}
    });

    <span class="hljs-title function_">expect</span>(response.<span class="hljs-property">tools</span>).<span class="hljs-title function_">toHaveLength</span>(<span class="hljs-number">2</span>);
    <span class="hljs-title function_">expect</span>(response.<span class="hljs-property">tools</span>[<span class="hljs-number">0</span>].<span class="hljs-property">name</span>).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">&quot;add_todo&quot;</span>);
    <span class="hljs-title function_">expect</span>(response.<span class="hljs-property">tools</span>[<span class="hljs-number">0</span>].<span class="hljs-property">inputSchema</span>.<span class="hljs-property">required</span>).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;task&quot;</span>);
  });
});

<span class="hljs-title function_">describe</span>(<span class="hljs-string">&quot;tools/call&quot;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;add_todo creates a todo&quot;</span>, <span class="hljs-title function_">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> server.<span class="hljs-title function_">handleRequest</span>({
      <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;tools/call&quot;</span>,
      <span class="hljs-attr">params</span>: {
        <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;add_todo&quot;</span>,
        <span class="hljs-attr">arguments</span>: { <span class="hljs-attr">task</span>: <span class="hljs-string">&quot;Test task&quot;</span>, <span class="hljs-attr">priority</span>: <span class="hljs-string">&quot;high&quot;</span> }
      }
    });

    <span class="hljs-title function_">expect</span>(response.<span class="hljs-property">content</span>[<span class="hljs-number">0</span>].<span class="hljs-property">text</span>).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;Test task&quot;</span>);
  });

  <span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;unknown tool throws error&quot;</span>, <span class="hljs-title function_">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> <span class="hljs-title function_">expect</span>(
      server.<span class="hljs-title function_">handleRequest</span>({
        <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;tools/call&quot;</span>,
        <span class="hljs-attr">params</span>: { <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;nonexistent&quot;</span>, <span class="hljs-attr">arguments</span>: {} }
      })
    ).<span class="hljs-property">rejects</span>.<span class="hljs-title function_">toThrow</span>(<span class="hljs-string">&quot;Unknown tool&quot;</span>);
  });
});

Level 3: Integration Testing with a Real Client

Use the MCP SDK's Client class:

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Client</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/client/index.js&quot;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">StdioClientTransport</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/client/stdio.js&quot;</span>;

<span class="hljs-keyword">const</span> transport = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StdioClientTransport</span>({
  <span class="hljs-attr">command</span>: <span class="hljs-string">&quot;node&quot;</span>,
  <span class="hljs-attr">args</span>: [<span class="hljs-string">&quot;./index.js&quot;</span>],
  <span class="hljs-attr">env</span>: { <span class="hljs-attr">OPENWEATHER_API_KEY</span>: <span class="hljs-string">&quot;test_key&quot;</span> }
});

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Client</span>({ <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;test&quot;</span>, <span class="hljs-attr">version</span>: <span class="hljs-string">&quot;1.0.0&quot;</span> }, {
  <span class="hljs-attr">capabilities</span>: {}
});

<span class="hljs-keyword">await</span> client.<span class="hljs-title function_">connect</span>(transport);

<span class="hljs-comment">// List tools</span>
<span class="hljs-keyword">const</span> { tools } = <span class="hljs-keyword">await</span> client.<span class="hljs-title function_">listTools</span>();
<span class="hljs-title function_">expect</span>(tools.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> t.<span class="hljs-property">name</span>)).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;get_current_weather&quot;</span>);

<span class="hljs-comment">// Call a tool</span>
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> client.<span class="hljs-title function_">callTool</span>({
  <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;get_current_weather&quot;</span>,
  <span class="hljs-attr">arguments</span>: { <span class="hljs-attr">city</span>: <span class="hljs-string">&quot;London&quot;</span> }
});
<span class="hljs-title function_">expect</span>(result.<span class="hljs-property">content</span>[<span class="hljs-number">0</span>].<span class="hljs-property">text</span>).<span class="hljs-title function_">toContain</span>(<span class="hljs-string">&quot;London&quot;</span>);

Level 4: Schema Validation

Test that your inputSchema is valid JSON Schema:

<span class="hljs-keyword">import</span> <span class="hljs-title class_">Ajv</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;ajv&quot;</span>;

<span class="hljs-keyword">const</span> ajv = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Ajv</span>();

<span class="hljs-title function_">it</span>(<span class="hljs-string">&quot;inputSchema is valid JSON Schema&quot;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> tool <span class="hljs-keyword">of</span> <span class="hljs-variable constant_">TOOLS</span>) {
    <span class="hljs-keyword">const</span> valid = ajv.<span class="hljs-title function_">validateSchema</span>(tool.<span class="hljs-property">inputSchema</span>);
    <span class="hljs-title function_">expect</span>(valid).<span class="hljs-title function_">toBe</span>(<span class="hljs-literal">true</span>);
  }
});

Testing Best Practices

  1. Extract pure logic — tool functions should be testable without MCP
  2. Mock external APIs — use nock or msw for HTTP calls
  3. Test error paths — invalid input, API failures, timeouts
  4. Validate schemas — JSON Schema validation catches regressions
  5. Run in CI — GitHub Actions, every push

Sample CI Config

<span class="hljs-comment"># .github/workflows/test.yml</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">Test</span>
<span class="hljs-attr">on:</span> [<span class="hljs-string">push</span>, <span class="hljs-string">pull_request</span>]
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">test:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v4</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>

Tested tools are trusted tools. Your users (and their AI agents) will thank you.

#MCP #Tutorial #Testing #Quality #mcpm