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.
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.
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:
# 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-yamlNext, create or update your tsconfig.json. The SDK requiresstrict: true and esModuleInterop: true:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}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.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.
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.
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
}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:
idnametypetestConnection()listTools()executeTool()getManifest()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{id}_describe_schemaMethods you must implement
| Property | Type | Required | Description |
|---|---|---|---|
connect() | Promise<void> | Required | Open the database connection (called automatically by init()). |
disconnect() | Promise<void> | Required | Close the connection and release resources. |
query(sql, params) | Promise<unknown> | Required | Execute a SQL/command query and return the result rows. |
describeSchema() | Promise<Record<string, unknown>> | Required | Return 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:
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:
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);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'.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
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Required | Unique connector slug. |
name | string | Required | Human-readable display name. |
version | string | Optional | Semantic version string (default "1.0.0"). |
description | string | Optional | Short description shown in the UI. |
type | ConnectorCategory | Optional | Defaults to "custom". |
supportedAuthTypes | AuthType[] | Optional | Which auth types this connector accepts. |
testHandler | (creds?) => Promise<ConnectionTestResult> | Optional | Custom 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:
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 };registerTool() returns this — you can chain calls fluently:tallyConnector.registerTool(tool1, h1).registerTool(tool2, h2).registerTool(tool3, h3);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)
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
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);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.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
Run the test suite
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
─── 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) 11mspackage.json so it runs in CI:"test": "ts-node src/test.ts"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.
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:
# 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 TokenAdds Authorization: Bearer <token> header. Use for JWT access tokens and OAuth2 flows.
{ type: 'bearer', token: process.env.MY_TOKEN }api_keyAPI KeyInjects a key into a header, query parameter, or cookie. Configure the parameter name in the auth config.
{ type: 'api_key', api_key: '...', parameter_name: 'X-API-Key', in: 'header' }basicBasic AuthStandard HTTP Basic Authentication. Adds Authorization: Basic base64(user:pass). Commonly used for database connectors.
{ type: 'basic', username: 'admin', password: process.env.DB_PASSWORD }oauth2_clientOAuth2 Client CredentialsMachine-to-machine OAuth2 flow. The platform exchanges client credentials for an access token automatically.
{ type: 'oauth2_client', token: process.env.ACCESS_TOKEN }custom_headerCustom HeaderInject any arbitrary header into every request. Useful for proprietary APIs that use non-standard auth headers.
{ type: 'custom_header', header_name: 'X-Tenant-ID', header_value: 'acme' }noneNo AuthFor public or internally trusted APIs that require no authentication.
{ type: 'none' }