Build an MCP
This guide walks through building, testing, and publishing a Cendriix MCP (Modular Capability Provider), the plugin system that gives agents access to external services, APIs, and tools.
What is an MCP?
An MCP is a self-contained package that exposes a typed toolset to the Cendriix orchestrator. Every tool call an agent makes goes through an MCP, the MCP validates inputs, enforces rate limits, handles authentication, and returns structured outputs. MCPs run in isolated sandboxes and cannot communicate with each other directly.
MCPs are written in TypeScript (Node 20+) and packaged as standard npm packages. The Cendriix catalog hosts verified MCPs for AWS, GitHub, Jira, Slack, Stripe, and many more. Custom MCPs can be published privately within your workspace or shared publicly with the community.
File layout
An MCP package follows a minimal, opinionated directory structure:
my-mcp/
cendriix.mcp.json # manifest (name, version, auth, tools)
src/
index.ts # entry point, exports the MCP class
tools/
read.ts # read tool implementation
write.ts # write tool implementation
list.ts # list tool implementation
watch.ts # watch tool implementation (optional)
package.json
tsconfig.jsonManifest schema
The cendriix.mcp.jsonmanifest is the single source of truth for the MCP's identity, version, authentication requirements, and tool declarations.
{
"name": "@acme/jira-mcp",
"version": "1.0.0",
"display": "Jira",
"description": "Read and write Jira issues, transitions, and comments.",
"auth": {
"kind": "oauth2",
"scopes": ["read:jira-work", "write:jira-work"]
},
"tools": [
{
"name": "jira.issue.read",
"description": "Fetch a Jira issue by key.",
"input": {
"type": "object",
"properties": {
"key": { "type": "string", "description": "Issue key, e.g. JIRA-3421." }
},
"required": ["key"]
},
"output": { "type": "object" }
},
{
"name": "jira.issue.write",
"description": "Update fields on a Jira issue.",
"input": {
"type": "object",
"properties": {
"key": { "type": "string" },
"fields": { "type": "object" }
},
"required": ["key", "fields"]
},
"output": { "type": "object" }
}
]
}Standard tool methods
Every MCP exposes up to four standard tool methods. You only need to implement the ones your integration requires.
| Method | Description | Required? |
|---|---|---|
read | Retrieve a single resource by identifier. Must be idempotent. | Recommended |
write | Create or update a resource. May have side effects. | Optional |
list | Enumerate resources, optionally with filters. | Optional |
watch | Subscribe to a resource stream (Server-Sent Events). Used for live monitoring tools. | Optional |
Here is a minimal implementation of the read method for a Jira issue tool:
// src/tools/read.ts
import type { McpContext, ReadInput, ReadOutput } from '@cendriix/mcp-sdk';
interface IssueReadInput extends ReadInput {
key: string;
}
interface JiraIssue {
id: string;
key: string;
fields: Record<string, unknown>;
}
export async function read(
input: IssueReadInput,
context: McpContext,
): Promise<ReadOutput<JiraIssue>> {
const baseUrl = context.config.get('jira_base_url') as string;
const token = await context.auth.getToken();
const res = await fetch(`${baseUrl}/rest/api/3/issue/${input.key}`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
if (!res.ok) {
throw new Error(`Jira API error ${res.status}: ${await res.text()}`);
}
const issue = await res.json() as JiraIssue;
return { data: issue };
}The entry point wires all tool handlers into a single class:
// src/index.ts
import { McpBase, type McpManifest } from '@cendriix/mcp-sdk';
import { read } from './tools/read.js';
import { write } from './tools/write.js';
import { list } from './tools/list.js';
export class JiraMcp extends McpBase {
static override manifest: McpManifest = {
name: '@acme/jira-mcp',
version: '1.0.0',
};
override read = read;
override write = write;
override list = list;
}Authentication
MCPs declare their auth requirement in the manifest. The orchestrator handles credential storage and token refresh transparently, your tool code callscontext.auth.getToken() and receives a fresh, scoped access token.
| Auth kind | Use case |
|---|---|
oauth2 | SaaS apps with standard OAuth flows (GitHub, Jira, Slack, Salesforce). |
api_key | Static API keys stored as workspace secrets. |
aws_iam | AWS services via the connected cross-account IAM role. |
none | Public APIs or MCPs that manage auth internally. |
context.config.get(key)for workspace-level secrets set via the dashboard or CLI (cendriix workspace secrets set JIRA_BASE_URL https://acme.atlassian.net).Packaging
MCPs are standard npm packages. Build and bundle with the Cendriix SDK toolchain:
# Install SDK
npm install @cendriix/mcp-sdk
# Validate manifest and run type checks
npx cendriix mcp validate
# Build a distributable bundle
npx cendriix mcp build
# Test locally against the orchestrator sandbox
npx cendriix mcp devThe mcp build command produces a single CommonJS bundle indist/, which is what the catalog distributes.
Publishing to the catalog
MCPs can be published to your private workspace catalog or to the global community catalog (open-source MCPs only).
# Publish to your private workspace catalog
npx cendriix mcp publish --visibility private
# Publish to the global community catalog
npx cendriix mcp publish --visibility publicCommunity catalog submissions are reviewed by the Cendriix security team. Review typically takes 2–5 business days. MCPs must pass automated security scanning, manifest validation, and a manual review of tool descriptions before listing.
Once published, workspace members can install your MCP from the MCP Catalog page or directly:
cendriix mcp install @acme/jira-mcp^1.0.0). Breaking changes must bump the major version.