Posts

Creating and Publishing MCP Kernel

Why I built MCP Kernel to share setup, dependencies, and request policies across TypeScript MCP servers.

September 18, 2026 4 min read 718 words

In this article

An MCP server needs more than a collection of tool handlers. Once you add service clients, shared context, authorization rules, and cleanup, you have an application to maintain.

I created and published MCP Kernel to handle that repeated code: tool registration, shared dependencies, request policies, startup, and cleanup. Your application supplies the API clients and handlers that do the actual work.

You can find it on GitHub and npm.

Why I Built It

Consider a server that exposes tools for reading project records and updating their status. Each handler has its own business logic, but several questions repeat:

  • Where does the service client come from?
  • Who checks the caller’s scopes?
  • What happens when a request times out?
  • Who releases dependencies when startup fails halfway through?

Copy those answers into every handler and you have several implementations of the same application rules. Eventually, one gets fixed and another gets forgotten. Copy-paste has excellent distribution and questionable maintenance support.

MCP Kernel groups features into plugins and manages their startup, invocation, and cleanup. The official MCP SDK handles the protocol and transports. The architecture guide covers the details.

MCP client
  <-> official SDK transport
  <-> kernel application
       -> middleware and tool policies
       -> feature handler
       -> your service client

Your application supplies credentials, service clients, and business behavior through its context.

What MCP Kernel Adds to the Official SDK

The SDK gives you the protocol APIs. You still need to decide how your application shares dependencies, applies policies, and cleans up resources. MCP Kernel provides those pieces.

Take the record tools above. Say you want reads cached for 30 seconds, every read to require the caller’s records:read scope, and updates to invalidate the cache. The read tool declares:

annotations: { readOnlyHint: true },
policy: {
  requiredScopes: ['records:read'],
  cache: { ttlMs: 30_000, tags: ['records'] },
},

The update tool declares policy: { invalidates: ['records'] }. The kernel checks scopes before serving cached data and invalidates tagged results after a successful write. Your handlers handle the records. The runtime policy documentation covers the ordering and options.

Caches and rate limits are local to the application, and the application supplies authenticated caller identity. Those details matter when deciding where these policies fit in your server.

That is the benefit I wanted: smaller handlers and one place to fix the behavior they share.

Defining a Tool and Plugin

Here is a small feature definition using the API documented for the published 0.1.1 release:

import { defineTool, jsonResult } from '@coderrob/mcp-kernel';
import { z } from 'zod';

interface AppContext {
  greeting: string;
}

const greet = defineTool<AppContext>()({
  name: 'greet_reader',
  description: 'Return a personalized greeting.',
  inputSchema: z.object({ name: z.string().min(1) }),
  outputSchema: z.object({ message: z.string() }),
  annotations: { readOnlyHint: true },
  handler: ({ input, context }) =>
    jsonResult({ message: `${context.greeting}, ${input.name}!` }),
});

The handler gets typed input and an explicit dependency, context.greeting. The output schema describes the result it returns.

Then, in the same module, compose the feature into an application factory:

import { createMcpServer, definePlugin } from '@coderrob/mcp-kernel';

export function createGreetingApp() {
  return createMcpServer<AppContext>({
    identity: { name: 'reader-greetings', version: '1.0.0' },
    plugins: [
      definePlugin({
        name: 'greetings',
        version: '1.0.0',
        features: [greet],
      }),
    ],
    createContext: () => ({ greeting: 'Hello' }),
  });
}

A process entry point starts the application with app.start(stdioTransport()) and calls app.stop() during shutdown. Keep logs on stderr; stdout carries MCP messages. A stray console.log can break the conversation.

The factory also lets each test start with a fresh application. connectTestClient connects an official SDK client through in-memory transports so tests can discover and call tools through MCP. The getting-started guide shows the startup and test setup.

Publishing the Package

MCP Kernel ships ESM, CommonJS, and bundled TypeScript declarations. Before publishing, the verification workflow checks that another TypeScript project can import the built package, that both runtime entry points load, and that the npm tarball contains the intended files. Passing tests inside the repository only gets me part of the way there; the installed package has to work too.

MCP Kernel is available under GPL-3.0-only. If you are building a TypeScript MCP server, take a look at the repository and try composing a few tools into a plugin.

I want the next tool I add to be mostly about what it does. I’ve written enough setup code to be happy maintaining less of it.

-Rob