Skip to main content
Glama
cheminfo

cheminfo-mcp-server

Official
by cheminfo

cheminfo-mcp-server

Serve tools to MCP clients and to the cheminfo chat from one definition, with the structures, reaction schemes, spectra and files they produce carried beside the answer, never through the model.

One tool definition gives a server three routes:

Route

For

Answers

GET tools

the chat, any client

the catalog: each tool's name, title, description, strict JSON Schema and hints

POST tools/{name}

the chat, the page

{ result, molecules, reactions?, spectra?, files? }

POST mcp

MCP clients

MCP's Streamable HTTP, stateless: a server per request, JSON answers

result is what the model reads. The other fields are drawn by the page or the chat beside it.

Installation

npm install cheminfo-mcp-server

The server entry needs fastify (5) and zod (4). cheminfo-mcp-server/client and cheminfo-mcp-server/contract need only zod, and run in a browser.

Related MCP server: wazza-mcp-test-server

A server

import { fastify } from 'fastify';
import {
  createToolRoutes,
  defineTool,
  wellKnownNotFound,
} from 'cheminfo-mcp-server';
import { z } from 'zod';

const mfInfo = defineTool({
  name: 'mfInfo',
  title: 'Molecular formula',
  description: 'Mass and composition of a molecular formula, e.g. "C6H12O6".',
  schema: z.strictObject({ mf: z.string() }),
  execute: ({ mf }) => ({
    result: { mf, monoisotopicMass: 180.0634 },
    molecules: [{ smiles: 'OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O' }],
  }),
});

const app = fastify();
await app.register(wellKnownNotFound);
await app.register(
  createToolRoutes({
    name: 'molecules',
    version: '1.0.0',
    instructions: 'Tools on molecules and molecular formulas.',
    tools: [mfInfo],
  }),
  { prefix: '/v1' },
);
  • A tool's schema is a strict zod object: an unknown parameter is refused. A parameter set to null is left out, and a single value under another name is moved to a tool's only parameter, because small models rename it.

  • A tool that throws answers 500 to the JSON route and an error result to MCP. The error itself never reaches the caller; onError is told.

  • A tool refuses a call with its own status by throwing new ToolRefusalError({ status: 503, code: 'loading', error: 'The copy is loading.', retryAfter: 30, extra: { mirror } }). The JSON route answers that status, { error, code, ...extra } and Retry-After; MCP answers error as an error result.

  • An unknown tool is a 404 before the caller is checked; unknownToolMessage words it.

  • Tools are read-only by default (READ_ONLY_ANNOTATIONS); a tool says otherwise with annotations.

  • tools may be a function, evaluated per request, when the descriptions change at run time.

  • routeConfig is merged into the config of the two POST routes, for @fastify/rate-limit.

Who is asking

A server whose tools need to know the caller resolves a context per request:

createToolRoutes<{ email: string }>({
  name: 'scipeaks-mcp',
  version,
  tools,
  resolveContext: async (request, route) => {
    const email = await checkToken(request.headers['x-eln-token']);
    return email === null
      ? {
          ok: false,
          refusal: {
            status: 401,
            code: 'no-credentials',
            error: 'Send your token.',
          },
        }
      : { ok: true, context: { email } };
  },
});

A refusal answers its status before MCP starts, with WWW-Authenticate: Bearer on a 401. A 503 is the exception: MCP still lists the tools, and each call answers the refusal's sentence, so a client sees the tools while the server catches up. route is 'mcp' or 'tools', for a server that accepts different credentials on each.

A browser Origin other than the page's own is refused on POST mcp unless listed in allowedOrigins.

A client

import { createToolClient } from 'cheminfo-mcp-server/client';

const molecules = createToolClient({
  baseUrl: 'https://molecules.cheminfo.org/v1/',
});
const catalog = await molecules.catalog();
const run = await molecules.callTool('mfInfo', { mf: 'C6H12O6' });
if (run.ok) {
  console.log(run.value.result, run.value.molecules);
} else {
  console.log(run.failure.kind, run.failure.message);
}

Every reply is checked against the contract. A malformed side channel is dropped, never the result. A redirect is never followed, so a credential never leaves for another address. A failure is refused (the server's own code and sentence, with Retry-After, and its whole refusal in reply, the server's own fields included), unreachable, timeout or invalid-reply.

The contract

cheminfo-mcp-server/contract holds the zod schemas and types every side shares: ToolCatalogSchema, ToolRunReplySchema, ErrorReplySchema, DisplayMoleculeSchema, DisplayReactionSchema, DisplaySpectrumSchema and DisplayFileSchema, with their limits (MAX_MOLECULES, MAX_REACTIONS, MAX_SPECTRA, MAX_FILES).

License

MIT

Related MCP Connectors

Related MCP Servers