@mcp-z/server
    Preparing search index...

    @mcp-z/server

    @mcp-z/server

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

    • 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
    npm install @mcp-z/server @modelcontextprotocol/server express
    

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

    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.

    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.

    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.

    • 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.

    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 }
    ]);

    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);

    Helpers for consistent tool inputs and output shaping:

    • createFieldsSchema() / parseFields() / filterFields()
    • createPaginationSchema()
    • createShapeSchema() / toColumnarFormat()
    • Node.js >= 20

    API Docs