Adding custom MCP tools & resources
Speakeasy SDK developers can augment MCP servers by adding extensions for additional custom tools, resources, and prompts.
Setting Up MCP Extensions
Creating a new file under the mcp-server
directory called server.extensions.ts
is required. The file must expose the following function contract exactly.
This function registers custom tools, resources, and prompts on the generated MCP server.
import { Register } from "./extensions.js";export function registerMCPExtensions(register: Register): void {register.tool(...);register.resource(...);register.prompt(...);}
After adding this file and defining custom tools and resources, execute speakeasy run
.
Building and Registering Custom Tools
The following example shows a custom MCP tool (opens in a new tab) that fetches files from public GitHub repositories.
This custom tool is registered in the registerMCPExtensions
function.
Custom resources can be defined in separate files or within the server.extensions.ts
file.
The tool must fit the ToolDefinition
type exposed by Speakeasy.
This example tool has args
defined as a Zod schema, though tools can also have no arguments defined.
Every tool requires a tool
method for execution.
Speakeasy provides a formatResult
utility function from tools.ts
to ensure the result conforms to the proper MCP format when returned. This function is optional as long as the return matches the required type.
import { z } from "zod";import { formatResult, ToolDefinition } from "../tools.js";type FetchGithubFileRequest = {org: string;repo: string;filepath: string;};const FetchGithubFileRequest$inboundSchema: z.ZodType<FetchGithubFileRequest,z.ZodTypeDef,unknown> = z.object({org: z.string(),repo: z.string(),filepath: z.string()});const fetchGithubFileToolArg = {request: FetchGithubFileRequest$inboundSchema};export const tool$fetchGithubFile: ToolDefinition<typeof fetchGithubFileToolArg> = {name: "admin_get_git_file",description: "Gets a file from a GitHub repository",scopes: [],args: fetchGithubFileToolArg,tool: async (_client, args, _extra) => {const { org, repo, filepath } = args.request;const url = `https://raw.githubusercontent.com/${org}/${repo}/main/${filepath}`;try {const response = await fetch(url);if (!response.ok) {throw new Error(`Failed to fetch file: ${response.statusText}`);}const content = await response.text();return formatResult(content, { response });} catch (err) {console.error(err);return {content: [{ type: "text", text: `Error: "${String(err)}"` }],isError: true,};}}};
import { tool$fetchGithubFile } from "./custom/getGithubFileTool.js";import { Register } from "./extensions.js";export function registerMCPExtensions(register: Register): void {register.tool(tool$fetchGithubFile);}
Building and Registering Custom Resources
An MCP Resource (opens in a new tab) represents a data source that an MCP server makes available to clients. Each resource is identified by a unique URI and can contain either text or binary data. Resources can encompass a variety of things, including:
- File contents (local or remote)
- Database records
- Screenshots and images
- Static API responses
- And more
The following example shows a custom MCP Resource that embeds a local PDF file as a resource into an MCP server.
The custom resource must fit the ResourceDefinition
or ResourceTemplateDefinition
type exposed by Speakeasy.
A resource must define a read
function for reading data from the defined URI.
Speakeasy provides a formatResult
utility function from resources.ts
to ensure the result conforms to the proper MCP format when returned. This function is optional as long as the return matches the required type.
import { formatResult, ResourceDefinition } from "../resources.js";import fs from 'node:fs/promises';export const resource$aboutSpeakeasy: ResourceDefinition = {name: "About Speakeasy",description: "Reads the about Speakeasy PDF",resource: "file:///Users/ryanalbert/about_speakeasy.pdf",scopes: [],read: async (_client, uri, _extra) => {try {const pdfContent = await fs.readFile(uri, null);return formatResult(pdfContent, uri, {mimeType: "application/pdf"});} catch (err) {console.error(err);return {contents: [{isError: true,uri: uri.toString(),mimeType: "application/json",text: `Failed to read PDF file: ${String(err)}`}]};}}}
import { resource$aboutSpeakeasy } from "./custom/aboutSpeakeasyResource.js";import { Register } from "./extensions.js";export function registerMCPExtensions(register: Register): void {register.resource(resource$aboutSpeakeasy);}
Building and Registering Custom Prompts
An MCP Prompt (opens in a new tab) enables creation of reusable prompt templates and workflows for MCP.
import { z } from "zod";import { formatResult, PromptDefinition } from "../prompts.js";const myNameArg = { first_name: z.string(), last_name: z.string() };export const prompt$aboutMyName: PromptDefinition<typeof myNameArg> = {name: "tell-me-about-my-name-prompt",description: "tell me about the origins of my name",args: myNameArg,prompt: (_client, args, _extra) => ({messages: [{role: "user",content: {type: "text",text: `Please tell me about the origin of my name first_name: ${args.first_name} last_name: ${args.last_name}`}}]})}const prompt$aboutSpeakeasy: PromptDefinition<undefined> = {name: "tell-me-about-speakeasy",prompt: (_client, _extra) =>formatResult("Please tell me about the company Speakeasy"),};
import { prompt$aboutMyName, prompt$aboutSpeakeasy } from "./custom/customPrompts.js";import { Register } from "./extensions.js";export function registerMCPExtensions(register: Register): void {register.prompt(prompt$aboutMyName);register.prompt(prompt$aboutSpeakeasy);}