PluginWorld
Se

server

MCP

Shared utilities and server orchestration for building MCP (Model Context Protocol) servers

@mcp-z · v2.2.3 · MIT · updated 10d ago

SECURITY

B

SCORE

67

INSTALLS

▲ 5.8K

PLUG IN

claude mcp add server -- npx -y @mcp-z/server

README

@mcp-z/server

Shared utilities and server orchestration for building MCP (Model Context Protocol) servers

Common uses

  • Parse transport config for stdio or HTTP
  • Wire MCP servers to stdio or Express HTTP
  • Compose auth/logging middleware
  • Serve generated files (PDFs, CSVs)
  • Build field/pagination/shape schemas

Install

npm install @mcp-z/server @modelcontextprotocol/server express

The example imports the MCP SDK and Express directly, so they are explicit application dependencies.

Quick start

import express from 'express';
import { McpServer } from '@modelcontextprotocol/server';
import { parseConfig, connectStdio, connectHttp } from '@mcp-z/server';

const config = parseConfig(process.argv.slice(2), process.env);
const writeLog = (...args: unknown[]) => console.error(...args);
const logger = { debug: writeLog, info: writeLog, warn: writeLog, error: writeLog };
const buildServer = () => {
  const server = new McpServer({ name: 'my-server', version: '1.0.0' });
  server.registerTool('hello', { description: 'Return a greeting' }, async () => ({
    content: [{ type: 'text', text: 'Hello from my server' }]
  }));
  return server;
};

if (config.transport.type === 'stdio') {
  await connectStdio(buildServer, { logger });
} else {
  const app = express();
  await connectHttp(buildServer, { logger, app, port: config.transport.port });
}

Keep logs on stderr. Writing logs to stdout corrupts the stdio protocol stream.

Protocol versions

connectHttp and connectStdio serve both the 2025 and 2026-07-28 MCP protocol revisions from the same server definitions, on the same HTTP endpoint and the same stdio connection. A legacy 2025 client keeps working unchanged; support for it is not being dropped.

Serving both revisions requires a factory, not an McpServer instance. The 2026-07-28 revision is stateless. A client speaking it sends no initialize handshake, and the SDK caches the negotiated revision on the McpServer instance. Its own documentation puts it plainly: once a version is negotiated, "a negotiated session never re-routes a method onto the other era." So a single shared instance pins itself to whichever revision reaches it first and answers the other with -32601 Method not found. A factory hands every request (HTTP) or connection (stdio) a fresh, un-negotiated instance, so each one negotiates for itself:

const buildServer = () => {
  const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
  // register tools/resources/prompts on mcpServer
  return mcpServer;
};

await connectStdio(buildServer, { logger });
// or
await connectHttp(buildServer, { logger, app, port: config.transport.port });

Passing a ready-made McpServer instance is still accepted and still compiles, but it is correct only for a server that will speak one revision. If both eras must work, pass a factory. A factory serves 2025-era clients exactly as an instance did, so there is no reason to prefer an instance once you have one.

Cache hints

On 2026-07-28 the SDK stamps ttlMs and cacheScope onto every cacheable result, defaulting to ttlMs: 0 / cacheScope: 'private'. This safe default disables caching. defaultCacheHints is a policy for a typical mcp-z server: catalogs are cacheable and shareable, anything derived from a user's account is not.

import { McpServer } from '@modelcontextprotocol/server';
import { defaultCacheHints } from '@mcp-z/server';

const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' }, { cacheHints: defaultCacheHints });

tools/list, prompts/list, resources/templates/list and server/discover are given a five-minute TTL and cacheScope: 'public', because each is fixed at registration and identical for every caller. resources/list and resources/read stay private with no TTL: those vary by account, and marking them public would let a shared cache serve one user's data to another. Override a single operation by spreading ({ ...defaultCacheHints, 'tools/list': { ttlMs: 0 } }). 2025-era responses are unaffected because the fields do not exist there.

Registration helpers

  • registerTools(server, tools)
  • registerResources(server, resources)
  • registerPrompts(server, prompts)

Each registers in name order, not the order the array happened to arrive in, so two servers built from the same modules produce identical tools/list results. That is what lets a 2026-07-28 client keep a cached catalog valid across a reconnect. The array you pass is not mutated.

Middleware composition

Use composeMiddleware with middleware layers (auth, logging, etc.):

import { composeMiddleware, createLoggingMiddleware } from '@mcp-z/server';

const logging = createLoggingMiddleware({ logger });
const composed = composeMiddleware({ tools, resources, prompts }, [
  { withTool: authMiddleware.withToolAuth, withResource: authMiddleware.withResourceAuth, withPrompt: authMiddleware.withPromptAuth },
  { withTool: logging.withToolLogging, withResource: logging.withResourceLogging, withPrompt: logging.withPromptLogging }
]);

File serving utilities

For servers that generate files (PDFs, CSVs, images):

  • reserveFile() - Reserve a file path for streaming writes
  • writeFile() - Write a buffer directly
  • getFileUri() - file:// or http:// URI based on transport
  • createFileServingRouter() - Express router to serve files
import { reserveFile, getFileUri, createFileServingRouter } from '@mcp-z/server';

const reservation = await reserveFile('report.csv', { resourceStoreUri: 'file:///tmp/files' });
const uri = getFileUri(reservation.storedName, transport, {
  resourceStoreUri: 'file:///tmp/files',
  baseUrl: 'https://example.com',
  endpoint: '/files'
});

const router = createFileServingRouter({ resourceStoreUri: 'file:///tmp/files' }, { contentType: 'text/csv' });
app.use('/files', router);

Schema helpers

Helpers for consistent tool inputs and output shaping:

  • createFieldsSchema() / parseFields() / filterFields()
  • createPaginationSchema()
  • createShapeSchema() / toColumnarFormat()

Requirements

  • Node.js >= 20

Documentation

API Docs

SIMILAR PLUGINS