HostMCP.dev · Developer Guide

Building Custom
MCP Connectors

This guide explains — from first principles — how to create your own Model Context Protocol (MCP) Connectors using the open-source@hostmcp-dev/connector-sdk. You will learn how to connect a PostgreSQL database, wrap a REST API, or build a fully custom integration so AI agents can discover and call your tools automatically.

TypeScript SDKApache-2.0OpenAPI 2 / 3MCP ProtocolOpen Source
Write Connector
Extend SDK class in TypeScript
Test Locally
Built-in compliance test suite
Register
POST to /connectors API
AI Calls It
Rust MCP endpoint executes tools

Before you begin

Make sure you have the following in place before starting. You only need Node.js and basic TypeScript knowledge — everything else is provided by the SDK.

Node.js 18+ and npm 8+
The SDK targets ES2020 and uses native fetch.
TypeScript 5.x
Install with npm install -D typescript.
A target system to connect to
e.g. PostgreSQL DB, a REST API, or a custom service.
HostMCP.dev API token (for deploy)
Needed only in Step 7 when you register the connector.
1

Install the SDK

Add the connector-sdk package and configure TypeScript

The Connector SDK is published as an npm package from the open-source monorepo atmcp-public/connector-sdk. Install it into your project:

terminalbash
# Install the SDK
npm install @hostmcp-dev/connector-sdk

# Install peer dependencies for PostgreSQL (only if building a DB connector)
npm install pg
npm install -D @types/pg

# If you're building an OpenAPI connector, no extra deps needed —
# the SDK bundles @apidevtools/swagger-parser and js-yaml

Next, create or update your tsconfig.json. The SDK requiresstrict: true and esModuleInterop: true:

tsconfig.jsonjson
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Note
During local development you can also npm link the SDK directly from the monorepo: cd mcp-public/connector-sdk && npm link, thennpm link @hostmcp-dev/connector-sdk in your project. This lets you make SDK changes and see them reflected immediately without publishing.
2

Core Concepts

Understand the IConnector interface and the BaseConnector lifecycle

Every MCP connector — regardless of whether it talks to a database, a REST API, or a custom service — must implement the same IConnector interface. This is the contract HostMCP.dev uses to discover tools and execute them on behalf of AI agents.

The IConnector Interface

Think of IConnector as a plugin contract. Implement it and your connector can be registered, tested, and called by any MCP-compatible AI agent.

src/types/connector.tstypescript
export interface IConnector {
  // ─── Identity ─────────────────────────────────────────────────────────────
  readonly id: string;            // slug used in URLs and tool names, e.g. "postgres-prod"
  readonly name: string;          // human-readable, e.g. "Production PostgreSQL"
  readonly type: ConnectorCategory; // 'api' | 'database' | 'custom' | 'saas' | 'file' | 'stream'

  // ─── Lifecycle ────────────────────────────────────────────────────────────
  init(config: ConnectorConfig): Promise<void>;
  // Called once to set up connections, parse specs, etc.

  // ─── Health ───────────────────────────────────────────────────────────────
  testConnection(credentials?: ResolvedAuthCredentials): Promise<ConnectionTestResult>;
  // Returns { ok: boolean, latencyMs?, message? }

  // ─── Tool Discovery ───────────────────────────────────────────────────────
  listTools(): Promise<MCPToolDefinition[]>;
  // Returns every callable tool this connector exposes.

  getTool(toolName: string): Promise<MCPToolDefinition | undefined>;
  // Convenience — BaseConnector implements this for you.

  // ─── Execution ────────────────────────────────────────────────────────────
  executeTool(
    request: ToolExecutionRequest,
    credentials?: ResolvedAuthCredentials
  ): Promise<ToolExecutionResult>;

  // ─── Metadata ─────────────────────────────────────────────────────────────
  getManifest(): ConnectorManifest;
  // Static metadata stored in the platform DB.
}

MCPToolDefinition — what a Tool looks like

Each tool you expose is described by an MCPToolDefinition. The AI agent reads these definitions to understand what the tool does and what arguments it needs. TheinputSchema is a standard JSON Schema object.

src/types/tool.tstypescript
export interface MCPToolDefinition {
  name: string;           // unique within this connector, e.g. "postgres-prod_query"
  description: string;    // plain-English description the LLM uses to decide when to call this tool
  inputSchema: JSONSchemaDefinition;   // JSON Schema for the arguments the tool accepts
  outputSchema?: JSONSchemaDefinition; // optional — JSON Schema for the response shape
  prompt?: string;        // extra natural-language hints for the AI planner
  category?: string;      // e.g. "database", "finance", "crm"
  tags?: string[];        // search tags
  isDangerous?: boolean;  // true if this tool writes, deletes, or has side-effects
  operationId?: string;   // set automatically when derived from an OpenAPI spec
}
Tip
Always set isDangerous: true on tools that INSERT, UPDATE, DELETE, or have any other write side-effects. The MCP UI will show a confirmation prompt, and the AI agent will ask for explicit user approval before calling them.

BaseConnector — what you actually extend

You never implement IConnector from scratch. Instead you extendBaseConnector, which handles the boilerplate:

init(config)

Stores config and flips the initialized flag. Call super.init(config) when overriding.

getTool(name)

Iterates listTools() and returns the matching tool. Already implemented — no override needed.

ensureInitialized()

Protected guard method. Call it at the top of listTools() and executeTool() to throw a clear error if init() was not called.

You must implement these abstract members in every subclass:

id
name
type
testConnection()
listTools()
executeTool()
getManifest()
3

Building a Database Connector

Extend DatabaseConnector to wrap PostgreSQL, MySQL, MongoDB, or any other database

The SDK ships with DatabaseConnector, an abstract class that sits betweenBaseConnector and your implementation. It provides two free MCP tools (_query and _describe_schema) out of the box, and calls yourconnect() method automatically inside init().

What DatabaseConnector gives you for free

{id}_query
Execute any parameterised read query against the database. Accepts a query string and optional params array.
{id}_describe_schema
Introspect the database schema — tables, columns, and types. Useful for letting an AI agent understand the data model.

Methods you must implement

PropertyTypeRequiredDescription
connect()Promise<void>RequiredOpen the database connection (called automatically by init()).
disconnect()Promise<void>RequiredClose the connection and release resources.
query(sql, params)Promise<unknown>RequiredExecute a SQL/command query and return the result rows.
describeSchema()Promise<Record<string, unknown>>RequiredReturn a map of table/collection names to their column definitions.

Full PostgreSQL example

Create a new file at src/connectors/postgres.connector.ts. We'll extendDatabaseConnector, plug in the pg library, and add one extra domain-specific tool on top of the two free ones:

src/connectors/postgres.connector.tstypescript
import {
  DatabaseConnector,
  DatabaseConnectorConfig,
  MCPToolDefinition,
  ConnectorManifest,
  ConnectionTestResult,
} from '@hostmcp-dev/connector-sdk';
import { Pool, PoolConfig } from 'pg';

export class PostgresConnector extends DatabaseConnector {
  // ── 1. Declare identity ─────────────────────────────────────────────────
  //    These are abstract in BaseConnector — you MUST declare them.
  public readonly id   = 'postgres-prod';
  public readonly name = 'PostgreSQL Production DB';

  private pool: Pool | null = null;

  // ── 2. connect() — called automatically inside init() ───────────────────
  //    This is where you create your connection pool.
  //    The config properties (host, port, database, user, password, ssl)
  //    are read from this.dbConfig which is populated by init().
  protected async connect(): Promise<void> {
    const cfg: PoolConfig = {
      host:     this.dbConfig.host     ?? 'localhost',
      port:     this.dbConfig.port     ?? 5432,
      database: this.dbConfig.database ?? 'mydb',
      user:     this.dbConfig.user     ?? 'postgres',
      password: this.dbConfig.password ?? '',
      ssl:      this.dbConfig.ssl ? { rejectUnauthorized: false } : false,
      max: 10,           // connection pool size
      idleTimeoutMillis: 30000,
    };

    this.pool = new Pool(cfg);

    // Test the connection immediately to fail fast on bad credentials
    const client = await this.pool.connect();
    client.release();

    console.log('[PostgresConnector] ✓ Connected to', cfg.host, cfg.database);
  }

  // ── 3. disconnect() — call this when your app shuts down ─────────────────
  public async disconnect(): Promise<void> {
    await this.pool?.end();
    this.pool = null;
  }

  // ── 4. query() — the engine for the free _query MCP tool ─────────────────
  //    Always use parameterised queries ($1, $2 ...) — never interpolate values
  //    into the SQL string directly, or you'll be vulnerable to SQL injection.
  public async query(sql: string, params: unknown[] = []): Promise<unknown> {
    if (!this.pool) throw new Error('Connector not connected. Call init() first.');
    const result = await this.pool.query(sql, params);
    return result.rows;
  }

  // ── 5. describeSchema() — powers the free _describe_schema MCP tool ───────
  //    Returns a map of { tableName: { columnName: dataType } } so an AI agent
  //    can understand what data is available without seeing real rows.
  public async describeSchema(): Promise<Record<string, unknown>> {
    const rows = await this.query(`
      SELECT table_name, column_name, data_type
      FROM information_schema.columns
      WHERE table_schema = 'public'
      ORDER BY table_name, ordinal_position
    `) as Array<{ table_name: string; column_name: string; data_type: string }>;

    // Reshape flat rows into a nested { table: { column: type } } map
    const schema: Record<string, Record<string, string>> = {};
    for (const row of rows) {
      if (!schema[row.table_name]) schema[row.table_name] = {};
      schema[row.table_name][row.column_name] = row.data_type;
    }
    return schema;
  }

  // ── 6. listTools() — optionally add domain-specific tools ─────────────────
  //    Call super.listTools() to include the two free tools, then push your own.
  //    Naming convention: always prefix with the connector id.
  public override async listTools(): Promise<MCPToolDefinition[]> {
    const defaults = await super.listTools();
    return [
      ...defaults,
      {
        name: 'postgres-prod_list_customers',
        description:
          'Fetch a paginated list of active customers from the customers table. ' +
          'Use this when the user asks to see or search customer records.',
        inputSchema: {
          type: 'object',
          properties: {
            limit:  { type: 'number', description: 'Maximum number of rows (default 20, max 200)' },
            offset: { type: 'number', description: 'Pagination offset, starts at 0' },
            search: { type: 'string', description: 'Optional partial match on email or name' },
          },
        },
        outputSchema: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id:    { type: 'string' },
              email: { type: 'string' },
              name:  { type: 'string' },
            },
          },
        },
        category:    'database',
        tags:        ['customers', 'crm', 'postgres'],
        isDangerous: false,   // read-only
      },
      {
        name: 'postgres-prod_deactivate_customer',
        description:
          'Set a customer record to inactive. This is a WRITE operation — ' +
          'always confirm with the user before calling.',
        inputSchema: {
          type: 'object',
          properties: {
            customer_id: { type: 'string', description: 'UUID of the customer to deactivate' },
          },
          required: ['customer_id'],
        },
        category:    'database',
        tags:        ['customers', 'admin'],
        isDangerous: true,    // WRITE — marked dangerous so the UI shows a confirm dialog
      },
    ];
  }

  // ── 7. executeTool() — route each tool call to the right handler ───────────
  //    The parent DatabaseConnector.executeTool() already handles the two default
  //    tools. We only need to handle our custom ones.
  public override async executeTool(
    request: { toolName: string; arguments: Record<string, unknown> },
    credentials?: unknown
  ) {
    this.ensureInitialized();

    if (request.toolName === 'postgres-prod_list_customers') {
      const limit  = (request.arguments['limit']  as number)  ?? 20;
      const offset = (request.arguments['offset'] as number)  ?? 0;
      const search = (request.arguments['search'] as string)  ?? '';

      const sql = search
        ? 'SELECT id, email, name FROM customers WHERE active = true AND (email ILIKE $3 OR name ILIKE $3) LIMIT $1 OFFSET $2'
        : 'SELECT id, email, name FROM customers WHERE active = true LIMIT $1 OFFSET $2';

      const params = search ? [limit, offset, `%${search}%`] : [limit, offset];
      const rows = await this.query(sql, params);
      return { success: true, data: rows };
    }

    if (request.toolName === 'postgres-prod_deactivate_customer') {
      const id = request.arguments['customer_id'] as string;
      await this.query('UPDATE customers SET active = false WHERE id = $1', [id]);
      return { success: true, data: { deactivated: id } };
    }

    // Fallback to parent for _query and _describe_schema
    return super.executeTool(request as never, credentials as never);
  }

  // ── 8. testConnection() — health check ────────────────────────────────────
  public async testConnection(): Promise<ConnectionTestResult> {
    const start = Date.now();
    try {
      await this.query('SELECT 1');
      return { ok: true, latencyMs: Date.now() - start, message: 'PostgreSQL is reachable' };
    } catch (err) {
      return { ok: false, latencyMs: Date.now() - start, message: (err as Error).message };
    }
  }

  // ── 9. getManifest() — static metadata stored in the platform DB ──────────
  public getManifest(): ConnectorManifest {
    return {
      id:          this.id,
      name:        this.name,
      version:     '1.0.0',
      description: 'Read/write connector for the production PostgreSQL database',
      type:        this.type,          // 'database' — inherited from DatabaseConnector
      author:      'Platform Team',
      supportedAuthTypes: ['basic', 'none'],
      defaultConfig: {
        host:     'localhost',
        port:     5432,
        database: 'mydb',
        ssl:      false,
      },
    };
  }
}

Using the connector in your code

Once the class is written, using it is straightforward. Call init() once at startup with the connection details, then call any tool by name:

src/index.tstypescript
import { PostgresConnector } from './connectors/postgres.connector';

async function main() {
  const db = new PostgresConnector();

  // Step A — Initialise (opens the connection pool via connect() internally)
  await db.init({
    host:     process.env.DB_HOST     ?? 'localhost',
    port:     Number(process.env.DB_PORT) || 5432,
    database: process.env.DB_NAME     ?? 'mydb',
    user:     process.env.DB_USER     ?? 'postgres',
    password: process.env.DB_PASSWORD ?? '',
    ssl:      process.env.DB_SSL === 'true',
  });

  // Step B — Health check
  const health = await db.testConnection();
  console.log('Health:', health);
  // → { ok: true, latencyMs: 3, message: 'PostgreSQL is reachable' }

  // Step C — See what tools are available
  const tools = await db.listTools();
  console.log('Available tools:', tools.map(t => t.name));
  // → ['postgres-prod_query', 'postgres-prod_describe_schema', 'postgres-prod_list_customers', ...]

  // Step D — Execute a tool (same way the MCP endpoint calls it)
  const customers = await db.executeTool({
    toolName: 'postgres-prod_list_customers',
    arguments: { limit: 5, search: 'acme' },
  });
  console.log('Customers:', customers.data);

  // Step E — Run a raw query through the built-in tool
  const result = await db.executeTool({
    toolName: 'postgres-prod_query',
    arguments: {
      query:  'SELECT id, amount FROM invoices WHERE status = $1 ORDER BY created_at DESC LIMIT $2',
      params: ['unpaid', 10],
    },
  });
  console.log('Unpaid invoices:', result.data);

  // Always clean up
  await db.disconnect();
}

main().catch(console.error);
Security
Never put raw passwords in your source code. Use process.env.DB_PASSWORD and load values from .env files locally. In production on GCP, reference a Secret Manager path instead: secretRef: 'projects/my-proj/secrets/db-password/versions/latest'.
4

Building a Custom Connector

Use CustomConnector.registerTool() for any service that isn't a DB or OpenAPI spec

Not every integration has a database or an OpenAPI spec.CustomConnector is a builder-style class where you register individual tools and their handlers one by one. Each handler is just an async function that receives the tool arguments and returns any value.

This is ideal for: ERP systems with proprietary APIs (Tally, SAP BAPI), message queues, file systems, in-house microservices, or composite tools that call multiple backends.

CustomConnector constructor options

PropertyTypeRequiredDescription
idstringRequiredUnique connector slug.
namestringRequiredHuman-readable display name.
versionstringOptionalSemantic version string (default "1.0.0").
descriptionstringOptionalShort description shown in the UI.
typeConnectorCategoryOptionalDefaults to "custom".
supportedAuthTypesAuthType[]OptionalWhich auth types this connector accepts.
testHandler(creds?) => Promise<ConnectionTestResult>OptionalCustom health-check function. If omitted, always returns ok: true.

Full Tally ERP example

Tally Prime exposes an HTTP bridge on localhost:9000. There's no OpenAPI spec, so we use CustomConnector and register tools manually:

src/connectors/tally.connector.tstypescript
import { CustomConnector, ResolvedAuthCredentials } from '@hostmcp-dev/connector-sdk';
import axios from 'axios';    // npm install axios

// ── 1. Construct the connector with identity and a custom health-check ──────
const tallyConnector = new CustomConnector({
  id:          'tally-erp',
  name:        'Tally Prime ERP',
  version:     '2.1.0',
  description: 'Connects to Tally Prime via the HTTP bridge running on port 9000',
  type:        'custom',
  supportedAuthTypes: ['basic'],

  // testHandler is optional — provide it to check connectivity when the user
  // clicks "Test Connection" in the platform UI
  testHandler: async (creds?: ResolvedAuthCredentials) => {
    try {
      await axios.get('http://localhost:9000/ping', {
        auth: {
          username: creds?.username ?? '',
          password: creds?.password ?? '',
        },
        timeout: 3000,
      });
      return { ok: true, message: 'Tally Prime is running and reachable on port 9000' };
    } catch (err) {
      return { ok: false, message: (err as Error).message };
    }
  },
});

// ── 2. Register Tool: getLedger ──────────────────────────────────────────────
//    registerTool() takes two arguments:
//      • MCPToolDefinition — describes the tool to the AI
//      • handler           — the async function that does the actual work
//
tallyConnector.registerTool(
  {
    name: 'tally-erp_get_ledger',
    description:
      'Retrieve ledger transactions from Tally Prime for a given account and date range. ' +
      'Use this when the user asks for account balances, debit/credit entries, or trial balance.',
    inputSchema: {
      type: 'object',
      properties: {
        ledgerName: {
          type: 'string',
          description: 'Exact name of the ledger as it appears in Tally (case-sensitive)',
        },
        fromDate: {
          type: 'string',
          description: 'Start date in YYYY-MM-DD format (e.g. "2026-04-01" for start of FY)',
        },
        toDate: {
          type: 'string',
          description: 'End date in YYYY-MM-DD format (e.g. "2026-09-30" for half-year)',
        },
      },
      required: ['ledgerName'],
    },
    outputSchema: {
      type: 'object',
      properties: {
        ledgerName: { type: 'string' },
        openingBalance: { type: 'number' },
        closingBalance:  { type: 'number' },
        transactions: { type: 'array' },
      },
    },
    category:    'finance',
    tags:        ['tally', 'ledger', 'accounts'],
    isDangerous: false,  // read-only
  },
  async (args) => {
    // The handler receives the typed arguments object
    const response = await axios.post('http://localhost:9000/ledger', {
      ledger:   args['ledgerName'],
      fromDate: args['fromDate'] ?? null,
      toDate:   args['toDate']   ?? null,
    });
    return response.data;
  }
);

// ── 3. Register Tool: postVoucher ────────────────────────────────────────────
//    This tool has isDangerous: true because it writes to Tally.
//    The platform UI will show a confirmation dialog before executing it.
tallyConnector.registerTool(
  {
    name: 'tally-erp_post_voucher',
    description:
      'Create a new voucher entry in Tally Prime. ' +
      'This is a write operation — always confirm the details with the user first.',
    inputSchema: {
      type: 'object',
      properties: {
        voucherType: {
          type:  'string',
          enum:  ['Payment', 'Receipt', 'Journal', 'Contra'],
          description: 'Type of accounting voucher to create',
        },
        amount: {
          type: 'number',
          description: 'Amount in INR (must be positive)',
        },
        debitLedger: {
          type: 'string',
          description: 'Name of the ledger to debit',
        },
        creditLedger: {
          type: 'string',
          description: 'Name of the ledger to credit',
        },
        narration: {
          type: 'string',
          description: 'Optional description for the voucher entry',
        },
      },
      required: ['voucherType', 'amount', 'debitLedger', 'creditLedger'],
    },
    category:    'finance',
    tags:        ['tally', 'voucher', 'accounting'],
    isDangerous: true,   // ← WRITE OPERATION — shows confirm dialog in the platform UI
  },
  async (args) => {
    const response = await axios.post('http://localhost:9000/voucher', args);
    return {
      voucherId:   response.data.id,
      status:      'created',
      voucherType: args['voucherType'],
      amount:      args['amount'],
    };
  }
);

// ── 4. Init once after all tools are registered ──────────────────────────────
//    For CustomConnector, init() just marks the connector as ready.
//    You can chain .registerTool() calls before calling init().
await tallyConnector.init();

export { tallyConnector };
Tip
registerTool() returns this — you can chain calls fluently:
tallyConnector.registerTool(tool1, h1).registerTool(tool2, h2).registerTool(tool3, h3);
5

Building an OpenAPI Connector

Auto-generate MCP tools from any OpenAPI 2.0 / 3.x specification

If your target system already has an OpenAPI (Swagger) specification, you don't have to define tools by hand. OpenApiConnector fetches and parses the spec, then automatically creates one MCP tool per API operation. The operation ID becomes the tool name, and all parameters / request-body fields become the inputSchema.

The spec can be a URL, a local YAML / JSON file path, or a parsed JavaScript object — the SDK handles all three.

Example: Stripe API (URL spec)

src/connectors/stripe.connector.tstypescript
import { OpenApiConnector } from '@hostmcp-dev/connector-sdk';

// Construct with the spec URL.  No other setup needed.
export const stripeConnector = new OpenApiConnector({
  id:          'stripe-payments',
  name:        'Stripe Payments API',
  spec:        'https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json',
  baseUrl:     'https://api.stripe.com',
  description: 'Full Stripe API for payments, customers, and subscriptions',
  version:     '2024-06-20',
});

// init() fetches the spec, parses every endpoint, and builds tool definitions.
// This is an async operation — await it before calling listTools().
await stripeConnector.init({
  auth: { type: 'bearer' },   // declares that this API uses Bearer tokens
                               // the actual token is provided at execute time
});

// Every GET/POST/PUT/DELETE operation in the Stripe spec is now a tool
const tools = await stripeConnector.listTools();
console.log(`Discovered ${tools.length} MCP tools from the Stripe spec`);
console.log('First 5 tools:', tools.slice(0, 5).map(t => `${t.name}: ${t.description}`));

// Execute a tool — pass credentials at runtime, not at init time.
// This keeps secrets out of the connector definition.
const customer = await stripeConnector.executeTool(
  {
    toolName: 'GetCustomer',       // operationId from the Stripe OpenAPI spec
    arguments: {
      customer: 'cus_ABC123XYZ',   // path parameter — filled in automatically
    },
  },
  {
    type:  'bearer',
    token: process.env.STRIPE_SECRET_KEY,  // injected from env / Secret Manager
  }
);

if (customer.success) {
  console.log('Customer:', customer.data);
} else {
  console.error('Error:', customer.error?.message);
}

Example: Internal API with local YAML spec

src/connectors/billing-api.connector.tstypescript
import { OpenApiConnector } from '@hostmcp-dev/connector-sdk';
import { readFileSync } from 'fs';
import yaml from 'js-yaml';

// Load and parse the YAML spec file yourself, then pass the object to the constructor.
// This avoids a network fetch and works in air-gapped environments.
const specContent = readFileSync('./specs/billing-api.yaml', 'utf8');
const specObject  = yaml.load(specContent) as Record<string, unknown>;

export const billingConnector = new OpenApiConnector({
  id:      'internal-billing-api',
  name:    'Internal Billing API',
  spec:    specObject,
  baseUrl: 'https://billing.internal.acme.com',
});

await billingConnector.init({
  auth: {
    type:           'api_key',
    parameter_name: 'X-Internal-Key',
    in:             'header',   // injected into every request header automatically
  },
});

// Inspect the parsed spec to see what was auto-discovered
const parsedSpec = billingConnector.getParsedSpec();
console.log('API title:', parsedSpec?.title);
console.log('Operations:', parsedSpec?.operations.map(op => `[${op.method.toUpperCase()}] ${op.path}`));

// Execute a generated tool (operationId from your YAML becomes the tool name)
const invoices = await billingConnector.executeTool(
  {
    toolName: 'listInvoices',
    arguments: { customer_id: '12345', status: 'unpaid' },
  },
  {
    type:           'api_key',
    api_key:        process.env.BILLING_API_KEY,
    parameter_name: 'X-Internal-Key',
    in:             'header',
  }
);

console.log('Unpaid invoices:', invoices.data);
Note
The baseUrl in the constructor can be overridden in init(config) viaconfig.baseUrl. If neither is set, the SDK falls back to theservers[0].url field in the OpenAPI spec itself.
6

Testing & Validating Your Connector

Use the built-in test runner to verify compliance before deploying

Before registering a connector with the platform, always run the built-in compliance test suite. It checks four things automatically: manifest completeness, connectivity, tool schema correctness, and (optionally) a live tool execution.

What the test runner checks

1
Manifest compliance
Verifies that getManifest() returns an object with id, name, and version fields.
2
Connection test
Calls testConnection() and asserts ok === true. If false, prints the error message.
3
List tools
Calls listTools() and verifies every tool has a name and an inputSchema. Reports how many tools were discovered.
4
Sample execution (optional)
Calls executeTool() with arguments you provide and checks that the result has success: true.

Run the test suite

src/test.tstypescript
import { runConnectorTests } from '@hostmcp-dev/connector-sdk';
import { PostgresConnector } from './connectors/postgres.connector';

async function runTests() {
  // 1. Create and initialise the connector
  const connector = new PostgresConnector();
  await connector.init({
    host:     process.env.DB_HOST     ?? 'localhost',
    database: process.env.DB_NAME     ?? 'mydb',
    user:     process.env.DB_USER     ?? 'postgres',
    password: process.env.DB_PASSWORD ?? '',
  });

  // 2. Run the compliance suite
  const report = await runConnectorTests(connector, {
    credentials: { type: 'none' },

    // The sampleExecution block is optional but strongly recommended.
    // It runs a real tool call to verify the connector is wired correctly end-to-end.
    sampleExecution: {
      toolName:  'postgres-prod_query',
      arguments: {
        query:  'SELECT current_database() AS db, current_user AS user',
        params: [],
      },
    },
  });

  // 3. Print the report
  console.log('\n─── Connector Test Report ──────────────────────────────');
  console.log('Connector :', report.connectorId);
  console.log('Result    :', report.passed + '/' + report.totalTests + ' passed');
  console.log('');

  for (const r of report.results) {
    const icon = r.passed ? '✅' : '❌';
    console.log(`  ${icon}  ${r.name.padEnd(45)} ${r.durationMs}ms`);
    if (!r.passed && r.error) {
      console.log(`       └─ Error: ${r.error}`);
    }
  }

  // 4. Exit non-zero if any test failed (useful in CI pipelines)
  if (report.failed > 0) process.exit(1);

  await connector.disconnect();
}

runTests().catch(err => { console.error(err); process.exit(1); });

Expected output

terminal outputbash
─── Connector Test Report ──────────────────────────────
Connector : postgres-prod
Result    : 4/4 passed

  ✅  Manifest compliance                            2ms
  ✅  Connection test                                4ms
  ✅  List tools (4 discovered)                      1ms
  ✅  Tool execution (postgres-prod_query)           11ms
Tip
Add the test script to your package.json so it runs in CI:
"test": "ts-node src/test.ts"
7

Register & Deploy

Register the connector with the platform backend, then call it via the Rust MCP endpoint

Once your connector passes the test suite, you register it with the platform backend via two API calls: one to create the connector record, and one per tool. After that, any MCP-compatible AI agent can discover and call your tools via the Rust MCP endpoint.

Step A — Create the connector record

Call POST /connectors with the connector manifest and connection config. The platform stores the config (with credential refs to Secret Manager) and returns a connector ID.

src/deploy.tstypescript
import { PostgresConnector } from './connectors/postgres.connector';

const API_BASE  = 'https://api.mcp-platform.internal';
const API_TOKEN = process.env.MCP_API_TOKEN;  // your platform API token

async function deploy() {
  // 1 — Initialise the connector locally to get its manifest and tools
  const connector = new PostgresConnector();
  await connector.init({ host: 'localhost', database: 'mydb', user: 'postgres' });

  const manifest = connector.getManifest();
  const tools    = await connector.listTools();

  // 2 — Register the connector (POST /connectors)
  const connRes = await fetch(`${API_BASE}/connectors`, {
    method: 'POST',
    headers: {
      'Content-Type':  'application/json',
      'Authorization': `Bearer ${API_TOKEN}`,
    },
    body: JSON.stringify({
      type:     manifest.type,        // 'database'
      manifest,
      config: {
        host:     process.env.DB_HOST,
        database: process.env.DB_NAME,
        user:     process.env.DB_USER,
        // IMPORTANT: reference Secret Manager instead of the raw password
        secretRef: 'projects/my-gcp-project/secrets/prod-db-password/versions/latest',
        ssl: true,
      },
    }),
  });

  if (!connRes.ok) throw new Error(`Failed to create connector: ${await connRes.text()}`);
  const { id: connectorId } = await connRes.json();
  console.log('✓ Connector registered:', connectorId);

  // 3 — Register each MCP tool (POST /tools)
  for (const tool of tools) {
    const toolRes = await fetch(`${API_BASE}/tools`, {
      method: 'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${API_TOKEN}`,
      },
      body: JSON.stringify({
        connector_id:  connectorId,
        name:          tool.name,
        description:   tool.description,
        input_schema:  tool.inputSchema,
        output_schema: tool.outputSchema ?? null,
        is_dangerous:  tool.isDangerous ?? false,
      }),
    });

    if (!toolRes.ok) {
      console.error(`  ✗ Failed to register tool ${tool.name}: ${await toolRes.text()}`);
    } else {
      console.log(`  ✓ Tool registered: ${tool.name}`);
    }
  }

  console.log(`\nDeployment complete. ${tools.length} tools registered.`);
  await connector.disconnect();
}

deploy().catch(console.error);

Step B — Call tools via the MCP endpoint

Once registered, any AI agent (Claude, GPT-4, Gemini with MCP support) can discover and call your tools through the Rust MCP endpoint. You can also call it directly for testing:

terminalbash
# List all registered tools available to AI agents
curl https://mcp.your-domain.com/mcp/tools \
  -H "Authorization: Bearer $MCP_TOKEN"

# Discover a specific connector's tools
curl "https://mcp.your-domain.com/mcp/registry?connector=postgres-prod" \
  -H "Authorization: Bearer $MCP_TOKEN"

# Execute a tool (the same call the AI agent makes)
curl -X POST https://mcp.your-domain.com/mcp/tools/postgres-prod_list_customers/execute \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "arguments": {
      "limit": 10,
      "search": "acme"
    }
  }'

# ── Expected response ─────────────────────────────────────────────────
# {
#   "success": true,
#   "data": [
#     { "id": "c-001", "email": "admin@acme.com", "name": "Acme Corp" },
#     ...
#   ],
#   "metadata": { "durationMs": 8 }
# }

Authentication Types

The SDK injects credentials into every HTTP request at execute time. You declare the scheme in init() (so the SDK knows how to apply it) and provide the actual credential values in each executeTool() call. Credentials are never stored by the platform.

bearerBearer Token

Adds Authorization: Bearer <token> header. Use for JWT access tokens and OAuth2 flows.

Credentials object{ type: 'bearer', token: process.env.MY_TOKEN }
api_keyAPI Key

Injects a key into a header, query parameter, or cookie. Configure the parameter name in the auth config.

Credentials object{ type: 'api_key', api_key: '...', parameter_name: 'X-API-Key', in: 'header' }
basicBasic Auth

Standard HTTP Basic Authentication. Adds Authorization: Basic base64(user:pass). Commonly used for database connectors.

Credentials object{ type: 'basic', username: 'admin', password: process.env.DB_PASSWORD }
oauth2_clientOAuth2 Client Credentials

Machine-to-machine OAuth2 flow. The platform exchanges client credentials for an access token automatically.

Credentials object{ type: 'oauth2_client', token: process.env.ACCESS_TOKEN }
custom_headerCustom Header

Inject any arbitrary header into every request. Useful for proprietary APIs that use non-standard auth headers.

Credentials object{ type: 'custom_header', header_name: 'X-Tenant-ID', header_value: 'acme' }
noneNo Auth

For public or internally trusted APIs that require no authentication.

Credentials object{ type: 'none' }