| name | mg-connector |
| description | Use when the user asks to add a connector, create a connector, implement a connector for a service, add a tool to a connector, update a connector tool, or asks about connector architecture. Trigger phrases include "add connector", "create connector", "new connector", "implement connector", "add tool to connector", "update connector tool", "connector architecture". |
Connector Development Skill
Guide for creating, extending, and maintaining connector modules in the ModelGuide platform. Connectors integrate external services (e-commerce, helpdesk, calendars) into the platform, exposing their capabilities as tools that AI agents invoke via MCP.
When NOT to use this skill
Before designing a full TypeScript connector module, check whether a mocked connector (ADR-013) is enough. Use the mocked path instead when:
- The backend doesn't exist (demo, sales deck, POC, dry-run call).
- Tool responses can be static fixtures — no conditional logic, no response trimming, no network call.
- The connector is only meant to drive an agent evaluation / simulation flow with coherent fake data.
In those cases, skip this skill entirely. Go straight to connectors.yaml with isMocked: true and inline tool definitions (see the mg-cli or build-agent skills — both have templates). The CLI upserts a connectors_catalog entry and inserts connector_tools rows with mock_response populated; executeTool() falls back to those payloads at runtime. No code, no deploy.
Use this skill when the connector must call a real backend at runtime.
Architecture Overview
The 3-File Module Pattern
Every connector lives at modelguide-api/src/features/connectors/catalog/{slug}/ and consists of exactly three files:
| File | Purpose |
|---|
client.ts | HTTP client factory bound to connector config. Custom error class + typed fetcher. |
handlers.ts | Tool execution functions. Each handler is wrapped in an error-handling HOF. |
index.ts | Manifest: connector metadata + tool definitions array. Default export is ConnectorManifest. |
System Flow
code manifest → registry.ts → sync.ts → DB connectors_catalog
↓
org creates connector instance (with config/secrets)
↓
AI agent calls tool via MCP → handler executes with resolved config
Key Type Contracts
All types are in modelguide-api/src/features/connectors/catalog/types.ts:
interface ToolExecutionContext {
config: Record<string, string>;
input: Record<string, unknown>;
organizationId: string;
connectorId: string;
}
interface ToolExecutionResult {
success: boolean;
data?: Record<string, unknown>;
error?: string;
}
interface ConnectorToolDefinition {
catalog: CatalogTool;
handler: (ctx: ToolExecutionContext) => Promise<ToolExecutionResult>;
}
interface ConfigFieldSchema {
type: "string" | "secret" | "number" | "boolean";
required: boolean;
description: string;
default?: string | number | boolean;
}
type ConnectorType = "api" | "webhook" | "database" | "messaging";
interface ConnectorManifest {
name: string;
slug: string;
description: string;
connectorType: ConnectorType;
configSchema: Record<string, ConfigFieldSchema>;
authMethods: string[];
iconUrl: string;
tools: ConnectorToolDefinition[];
}
CatalogTool (from @db/schema/core):
interface CatalogTool {
name: string;
description: string;
inputSchema: Record<string, unknown>;
defaultRequiresConfirmation: boolean;
defaultTimeoutSeconds: number;
}
Creating a New Connector
Step 0: Gather Requirements
If .modelguide/CONNECTOR_HANDOFF.md exists, read it first and use it as the
source of truth for the requested service, auth, operations, and requested org
connector slug. Only ask follow-up questions for missing details.
Ask the user for:
- Service name and slug (lowercase, alphanumeric + hyphens)
- Description of what the connector does
- Connector type:
api | webhook | database | messaging
- Config fields: what the connector needs (base URL, API key, etc.)
- Auth method:
api_key, oauth2, bearer_token, etc.
- Initial tools: list of operations to expose
Build-Agent Handoff Contract
When this skill is invoked from /build-agent, update
.modelguide/CONNECTOR_HANDOFF.md in place instead of returning the result only
in prose.
Do not delete the original request fields. Preserve serviceName,
serviceSlug, requestedConnectorSlug, authModel, baseUrl, and
operations so build-agent can resume without losing the builder's API summary.
Minimum required fields on completion:
status: completed or status: blocked
catalogSlug — the new catalog connector slug
connectorSlug — the org connector instance slug to use in agents.yaml and sops.yaml
toolSlugs — exact tool slugs exposed by the connector
configFields — objects with name, description, and required for each
non-secret field that connectors.yaml must provide
secretFields — objects with field, name, and type for each secret
that mg setup will prompt for
changedFiles — exact repo file paths you modified
verification — commands run, or (pending) if verification was skipped
blocker — only when status: blocked
Important: catalogSlug and connectorSlug are different. catalogSlug
identifies the connector type in the global catalog. connectorSlug is the org
instance slug that becomes the MCP prefix at runtime. serviceSlug is only the
interview-time service identifier and must not be used as either of those final
slugs.
Step 1: Create client.ts
interface {Name}FetchOptions {
method?: string;
body?: Record<string, unknown>;
params?: Record<string, string | number | undefined>;
}
export class {Name}ApiError extends Error {
constructor(
public status: number,
public body: string,
) {
super(`{Name} API error ${status}: ${body}`);
this.name = "{Name}ApiError";
}
}
export type {Name}Fetcher = <T = unknown>(
path: string,
options?: {Name}FetchOptions,
) => Promise<T>;
export function create{Name}Fetcher(
config: Record<string, string>,
): {Name}Fetcher {
const baseUrl = config.baseUrl?.replace(/\/+$/, "");
if (!baseUrl) {
throw new Error("{Name} baseUrl is required");
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
};
if (config.apiKey) {
headers["Authorization"] = `Bearer ${config.apiKey}`;
}
return async function fetch<T = unknown>(
path: string,
options?: {Name}FetchOptions,
): Promise<T> {
const { method = "GET", body, params } = options ?? {};
let url = `${baseUrl}${path}`;
if (params) {
const entries = Object.entries(params).filter(([, v]) => v !== undefined);
if (entries.length > 0) {
const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
url += `?${qs}`;
}
}
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const text = await response.text();
throw new {Name}ApiError(response.status, text);
}
return response.json() as Promise<T>;
};
}
Important: The inner function name must NOT shadow globalThis.fetch. Use a distinct name like {slug}Fetch (e.g. medusaFetch).
Step 2: Create handlers.ts
import type { ToolExecutionContext, ToolExecutionResult } from "../types";
import {
{Name}ApiError,
type {Name}Fetcher,
create{Name}Fetcher,
} from "./client";
function errorResult(err: unknown): ToolExecutionResult {
if (err instanceof {Name}ApiError) {
return { success: false, error: `{Name} API ${err.status}: ${err.body}` };
}
const message = err instanceof Error ? err.message : String(err);
return { success: false, error: message };
}
function with{Name}(
fn: (
fetcher: {Name}Fetcher,
ctx: ToolExecutionContext,
) => Promise<ToolExecutionResult>,
): (ctx: ToolExecutionContext) => Promise<ToolExecutionResult> {
return async function handler(ctx) {
try {
const fetcher = create{Name}Fetcher(ctx.config);
return await fn(fetcher, ctx);
} catch (err) {
return errorResult(err);
}
};
}
export const listSomething = with{Name}(async (fetcher, ctx) => {
const input = ctx.input as { };
const data = await fetcher<Record<string, unknown>>("/api/endpoint", {
params: { },
});
return { success: true, data };
});
Rules:
- Never throw from handlers — always return
{ success: false, error } via the wrapper
- Type-cast
ctx.input to the expected shape matching the tool's inputSchema
- Use the
with{Name}() wrapper for every exported handler
Step 3: Create index.ts
import type { ConnectorManifest, ConnectorToolDefinition } from "../types";
import { listSomething } from "./handlers";
const tools: ConnectorToolDefinition[] = [
{
catalog: {
name: "List Something",
description: "Description of what this tool does",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "integer", description: "Max results", minimum: 1, maximum: 100 },
},
required: [],
},
defaultRequiresConfirmation: false,
defaultTimeoutSeconds: 30,
},
handler: listSomething,
},
];
const {slug}Manifest: ConnectorManifest = {
name: "{Display Name}",
slug: "{slug}",
description: "...",
connectorType: "api",
configSchema: {
baseUrl: {
type: "string",
required: true,
description: "API base URL",
},
apiKey: {
type: "secret",
required: true,
description: "API key for authentication",
},
},
authMethods: ["api_key"],
iconUrl: "https://example.com/icon.svg",
tools,
};
export default {slug}Manifest;
Step 4: Register in Registry
Add the new connector import to modelguide-api/src/features/connectors/catalog/registry.ts:
export async function loadAllManifests(): Promise<ConnectorManifest[]> {
const modules = await Promise.all([
import("./medusa/index"),
import("./{slug}/index"),
]);
}
Step 5: Create Unit Tests
Create modelguide-api/tests/unit/connectors/{slug}-handlers.test.ts:
import { afterAll, describe, expect, mock, test } from "bun:test";
import {
listSomething,
} from "@features/connectors/catalog/{slug}/handlers";
import type { ToolExecutionContext } from "@features/connectors/catalog/types";
const BASE_CONFIG: Record<string, string> = {
baseUrl: "https://api.test-service.com",
apiKey: "test_key_123",
};
function makeCtx(
input: Record<string, unknown> = {},
config = BASE_CONFIG,
): ToolExecutionContext {
return {
config,
input,
organizationId: "org-1",
connectorId: "conn-1",
};
}
const originalFetch = globalThis.fetch;
let fetchMock: ReturnType<typeof mock>;
function mockFetchSuccess(responseData: Record<string, unknown>) {
fetchMock = mock(() =>
Promise.resolve(
new Response(JSON.stringify(responseData), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
),
);
globalThis.fetch = fetchMock as typeof fetch;
}
function mockFetchError(status: number, body: string) {
fetchMock = mock(() => Promise.resolve(new Response(body, { status })));
globalThis.fetch = fetchMock as typeof fetch;
}
afterAll(() => {
globalThis.fetch = originalFetch;
});
describe("{Name} handlers", () => {
describe("listSomething", () => {
test("calls GET /api/endpoint", async () => {
mockFetchSuccess({ items: [] });
const result = await listSomething(makeCtx());
expect(result.success).toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, opts] = fetchMock.mock.calls[0];
expect(url).toContain("/api/endpoint");
expect(opts.method).toBe("GET");
});
});
describe("error handling", () => {
test("returns error on API 404", async () => {
mockFetchError(404, '{"message":"Not found"}');
const result = await listSomething(makeCtx({ id: "bad" }));
expect(result.success).toBe(false);
expect(result.error).toContain("404");
});
test("returns error when baseUrl is missing", async () => {
const result = await listSomething(makeCtx({}, { apiKey: "key" }));
expect(result.success).toBe(false);
expect(result.error).toContain("baseUrl");
});
test("returns error on network failure", async () => {
fetchMock = mock(() => Promise.reject(new Error("Network error")));
globalThis.fetch = fetchMock as typeof fetch;
const result = await listSomething(makeCtx());
expect(result.success).toBe(false);
expect(result.error).toContain("Network error");
});
});
});
Step 6: Verify
make api-typecheck
make api-test-unit
Step 7: Sync to Database
cd modelguide-api && bun run src/features/connectors/catalog/sync.ts
Adding Tools to an Existing Connector
- Read the existing manifest (
index.ts) and handlers (handlers.ts) to understand current tools
- Add handler in
handlers.ts using the existing with{Name}() wrapper
- Add tool definition to the
tools array in index.ts with full catalog metadata
- Add tests for the new handler in the existing test file
- Run
make api-typecheck && make api-test-unit
- Sync to update the DB catalog
Modifying Existing Tools
- Read the current tool definition in
index.ts and its handler in handlers.ts
- Update
inputSchema if changing inputs (JSON Schema format)
- Update handler logic in
handlers.ts
- Update
defaultRequiresConfirmation / defaultTimeoutSeconds if behavior changed
- Update tests in the corresponding test file
- Run
make api-typecheck && make api-test-unit
- Sync to push changes to the DB
Standards & Conventions
Naming
- Tool names: Human-readable in
catalog.name (e.g. "List Products")
- Tool slugs: Auto-derived from name as snake_case (e.g. "list_products")
- MCP tool names:
{connectorSlug}_{toolSlug} (e.g. "glowbox_store_list_products")
Error Handling
- Always use the
with{Name}() HOF wrapper — never throw from handlers
- Return
{ success: false, error: "message" } on failure
- The wrapper catches exceptions from the client and formats them consistently
Input Schemas
- JSON Schema format with
type, properties, required
- Add
description on every property
- Use
minimum/maximum for numeric bounds
- Nested objects are supported (see Medusa's
setDeliveryAddress)
Config Schema
type: "string" — plain text config (base URLs, region codes)
type: "secret" — encrypted in the secrets table, resolved at runtime
type: "number" — numeric config
type: "boolean" — feature flags
Confirmation & Timeouts
defaultRequiresConfirmation: true for side effects: orders, payments, deletions, state mutations
defaultRequiresConfirmation: false for read-only operations
defaultTimeoutSeconds: 30 for reads
defaultTimeoutSeconds: 60 for writes and complex operations
Client Pattern
- Factory function scoped to config:
create{Name}Fetcher(config)
- Custom error class:
{Name}ApiError with status and body
- Consistent headers (Content-Type, Accept, auth)
- Strip trailing slashes from base URL
- Inner fetch function name must NOT shadow
globalThis.fetch
Key Reference Files
| Purpose | Path |
|---|
| Type contracts | modelguide-api/src/features/connectors/catalog/types.ts |
| Registry | modelguide-api/src/features/connectors/catalog/registry.ts |
| Sync script | modelguide-api/src/features/connectors/catalog/sync.ts |
| DB schema (CatalogTool) | modelguide-api/src/db/schema/core.ts |
| Medusa manifest | modelguide-api/src/features/connectors/catalog/medusa/index.ts |
| Medusa handlers | modelguide-api/src/features/connectors/catalog/medusa/handlers.ts |
| Medusa client | modelguide-api/src/features/connectors/catalog/medusa/client.ts |
| Medusa tests | modelguide-api/tests/unit/connectors/medusa-handlers.test.ts |
| Connector service | modelguide-api/src/features/connectors/connectors.service.ts |
| MCP handler | modelguide-api/src/features/mcp/mcp.handler.ts |
| MCP service | modelguide-api/src/features/mcp/mcp.service.ts |