Skip to main content

Command Palette

Search for a command to run...

From a GraphQL schema to MCP tools with Simfinity.js

Updated
6 min readView as Markdown

A GraphQL schema already describes argument types and result fields. That makes it a useful starting point for exposing an API through MCP. The remaining choices are concrete: which operations clients should see, what each tool returns, and where authorization runs.

I contribute to Simfinity.js. Its optional @simtlix/simfinity-mcp package can generate tools from a GraphQL schema. It works independently of the PostgreSQL and MongoDB adapters, so this example starts with an ordinary GraphQL schema and a small in-memory catalog.

We'll inspect the generated tool, call it directly, and then expose the same operation over stdio. No database or model API key is needed for this example.

The complete example is available as a Gist. Download its ZIP if you prefer to start with the files already in place.

Start with a schema you can inspect

In an empty directory, run:

npm init -y
npm pkg set type=module
npm install @simtlix/simfinity-mcp@3.2.0 graphql@^16.11.0

Create schema.js:

import {
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
} from 'graphql';

const starters = [
  { id: 'pg', name: 'PostgreSQL starter', database: 'postgresql' },
  { id: 'mongo', name: 'MongoDB starter', database: 'mongodb' },
];

const Starter = new GraphQLObjectType({
  name: 'Starter',
  fields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    name: { type: new GraphQLNonNull(GraphQLString) },
    database: { type: new GraphQLNonNull(GraphQLString) },
  },
});

export const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      starters: {
        type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Starter))),
        description: 'Find the demo starter projects by database.',
        args: { database: { type: GraphQLString } },
        resolve: (_source, { database }) => starters.filter(
          (starter) => !database || starter.database === database,
        ),
      },
      serviceStatus: {
        type: GraphQLString,
        resolve: () => 'demo',
      },
    },
  }),
});

The data is a two-row demo catalog. The starters resolver accepts a database name and returns matching rows. A second root field, serviceStatus, will let us check that the MCP configuration exposes only the operation we choose.

In an application, the resolver could use your existing data access code. With a Simfinity-generated schema, pass the built schema once type registration and database initialization have completed.

Choose the operation and result fields

Create options.js:

export const options = {
  include: ['starters'],
  toolNamePrefix: 'catalog_',
  toolOverrides: {
    starters: {
      description: 'Find the demo starter projects by database.',
      selection: 'id name database',
    },
  },
  limits: { maxResultBytes: 8192 },
};

include contains the GraphQL root field name. The prefix changes the name presented to MCP clients to catalog_starters; the underlying GraphQL field remains starters.

The explicit selection fixes the result fields to id, name, and database. Clients supply arguments for that operation. They do not supply an arbitrary GraphQL selection through this tool.

This selection also means the generator omits its automatic outputSchema for the tool. Successful calls still provide structuredContent, which we'll use below.

The 8 KB limit caps the logical result payload. It is useful when choosing how much data to return, but it is not a database execution limit and cannot undo a mutation that already ran.

Inspect and call the generated tool

Create inspect.js:

import { generateMCPTools } from '@simtlix/simfinity-mcp';
import { schema } from './schema.js';
import { options } from './options.js';

const { tools, callTool, getOperation } = generateMCPTools(schema, options);

console.log('Tools:', tools.map((tool) => tool.name));
console.log('Input schema:', JSON.stringify(tools[0].inputSchema, null, 2));
console.log('GraphQL operation:', getOperation('catalog_starters'));

const result = await callTool('catalog_starters', { database: 'postgresql' });
if (result.isError) throw new Error(JSON.stringify(result.content));
console.log('Result:', JSON.stringify(result.structuredContent, null, 2));

Run it:

node inspect.js

The tool list contains one entry:

Tools: [ 'catalog_starters' ]

serviceStatus is absent. The generated input schema has an optional, nullable string named database, matching the GraphQL argument. The generated operation is:

query startersOperation($database: String) {
  starters(database: $database) { id name database }
}

Calling the tool with database: 'postgresql' returns:

{
  "starters": [
    {
      "id": "pg",
      "name": "PostgreSQL starter",
      "database": "postgresql"
    }
  ]
}

This is a useful place to check the mapping before involving a client. You can see the published name, generated input contract, GraphQL document, and actual result together.

The direct call does not require the MCP SDK. Install the SDK when you're ready to use a protocol transport.

Serve the same tool over stdio

Install the optional transport dependency:

npm install "@modelcontextprotocol/sdk@^1.13.0"

Create stdio.js:

import { startStdioMCPServer } from '@simtlix/simfinity-mcp';
import { schema } from './schema.js';
import { options } from './options.js';

await startStdioMCPServer(schema, {
  ...options,
  serverName: 'starter-catalog',
  serverVersion: '1.0.0',
});

Configure your MCP client's local server command to launch node with the absolute path to stdio.js. For example, the command's argument would be /path/to/mcp-example/stdio.js on a POSIX system or C:\path\to\mcp-example\stdio.js on Windows.

The relative imports resolve from the entry-point file, so the client does not need to start in the example directory. Use the full path to your Node executable if the client cannot find it through its environment.

Keep application logs off stdout in this entry point, because stdout carries MCP protocol messages. Use console.error for diagnostics. The separate inspect.js script can print normally because it is not a protocol server.

The client should discover catalog_starters and be able to call it with:

{ "database": "postgresql" }

Bring application rules with the schema

This demo reads an in-memory array and has no user accounts. Before replacing it with application data, decide which identity reaches the resolvers and how that identity is verified.

The operation allowlist controls which tools are published. Authorization still belongs in the application. For standalone Simfinity MCP execution, the schemaPlugins option can install schema hooks such as the auth plugin; creating a plugin object alone does not apply it. The application must supply trusted context. Tool arguments should never be treated as proof of the caller's identity.

If you add mutations later, account for the same transaction requirements as the GraphQL API. Cancellation does not roll back database work that has already started. For an HTTP transport, follow the documented authentication and host/origin configuration rather than deploying this local stdio example as a public endpoint.

I checked this example with the published @simtlix/simfinity-mcp 3.2.0 package on Node.js 24.15.0. The checks covered direct execution, filtering, a nullable argument, rejection of an invalid argument type, and discovery and invocation through an actual MCP stdio client. The exposed tool list contained only catalog_starters.

The MCP guide covers the database-backed setup and HTTP transport. The MCP reference documents the remaining generation and execution options. For a complete API with storage, start with the PostgreSQL or MongoDB quick start.