← back to Filemaker Mcp

src/index.js

59 lines

#!/usr/bin/env node
// FileMaker Cloud MCP server — exposes read tools freely and write tools behind
// a dry-run gate. Claude calls these as native tools once registered in
// ~/.claude.json. Env (FM_CLARIS_*, FM_CLOUD_HOST) comes from the MCP `env` block.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import * as fm from './fm-client.js';

const server = new McpServer({ name: 'filemaker', version: '0.1.0' });
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
const fail = (e) => ({ isError: true, content: [{ type: 'text', text: `ERROR: ${e.message}` }] });

server.tool('fm_databases', 'List the FileMaker Cloud files this connector is allowed to touch.',
  {}, async () => { try { return ok(fm.listDatabases()); } catch (e) { return fail(e); } });

server.tool('fm_layouts', 'List the layouts in a FileMaker file (the Data API reads/writes through layouts).',
  { database: z.string() },
  async ({ database }) => { try { return ok(await fm.layouts(database)); } catch (e) { return fail(e); } });

server.tool('fm_field_metadata', 'List the fields available on a given layout.',
  { database: z.string(), layout: z.string().optional() },
  async ({ database, layout }) => { try { return ok(await fm.fieldMetadata(database, layout)); } catch (e) { return fail(e); } });

server.tool('fm_get_record', 'Fetch one record by its internal FileMaker recordId.',
  { database: z.string(), layout: z.string().optional(), recordId: z.union([z.string(), z.number()]) },
  async ({ database, layout, recordId }) => { try { return ok(await fm.getRecord(database, layout, recordId)); } catch (e) { return fail(e); } });

server.tool('fm_list_records', 'List records from a layout (paged).',
  { database: z.string(), layout: z.string().optional(), limit: z.number().optional(), offset: z.number().optional() },
  async ({ database, layout, limit, offset }) => { try { return ok(await fm.listRecords(database, layout, { limit, offset })); } catch (e) { return fail(e); } });

server.tool('fm_find',
  'Find records. query is a FileMaker find object or array, e.g. {"Account Number":"1061921"} or [{"State":"ME"}].',
  { database: z.string(), layout: z.string().optional(), query: z.any(),
    limit: z.number().optional(), offset: z.number().optional(), sort: z.any().optional() },
  async ({ database, layout, query, limit, offset, sort }) => {
    // some MCP clients deliver z.any() params as JSON strings — FileMaker needs the real object
    if (typeof query === 'string') { try { query = JSON.parse(query); } catch { /* leave as-is */ } }
    if (typeof sort === 'string') { try { sort = JSON.parse(sort); } catch { /* leave as-is */ } }
    try { return ok(await fm.findRecords(database, layout, query, { limit, offset, sort })); } catch (e) { return fail(e); } });

// --- writes: dryRun defaults TRUE so a value never changes without an explicit second call ---
server.tool('fm_update_record',
  'Update fields on a record. dryRun:true (default) returns the before→after diff WITHOUT committing; pass dryRun:false to commit. Always run dryRun first and confirm the diff with Steve before committing.',
  { database: z.string(), layout: z.string().optional(), recordId: z.union([z.string(), z.number()]),
    fieldData: z.record(z.any()), dryRun: z.boolean().optional() },
  async ({ database, layout, recordId, fieldData, dryRun = true }) => {
    try { return ok(await fm.updateRecord(database, layout, recordId, fieldData, { dryRun })); } catch (e) { return fail(e); } });

server.tool('fm_create_record',
  'Create a record. dryRun:true (default) previews without writing; pass dryRun:false to create.',
  { database: z.string(), layout: z.string().optional(), fieldData: z.record(z.any()), dryRun: z.boolean().optional() },
  async ({ database, layout, fieldData, dryRun = true }) => {
    try { return ok(await fm.createRecord(database, layout, fieldData, { dryRun })); } catch (e) { return fail(e); } });

await server.connect(new StdioServerTransport());
console.error('filemaker-mcp server up');