sdxc
SearchCtrlK

[ The sdxc command ]

Make an MCP tool or resource

Write an MCP tool, or a resource with --resource, with sdxc make mcp, declared beside the others and mapped on the app's MCP server through lazy().

Last updated 2026-10-19

An MCP tool is something an AI agent can call on your app, such as searching posts; a resource is something it can read, such as an article. sdxc make mcp writes the handler in app/mcp/tools/ or app/mcp/resources/, declares it beside the others and maps it on the MCP server the template serves at /mcp:

sdxc make mcp search-posts
sdxc make mcp article --resource
Wrote app/mcp/tools/search-posts.ts
Wrote app/mcp/tools/search-posts.test.ts
Edited app/mcp/tools/index.ts
Edited app/initializers/mcp.ts

Flags

FlagDefaultWhat it does
--resourcewrite a resource in app/mcp/resources/ instead of a tool
--test, --no-testmake.testswrite the test beside it, or skip it
--dry-runprint the files and the diffs, and change nothing

A tool

The handler answers with text, for you to replace with what the tool returns:

app/mcp/tools/search-posts.ts
import { createTool } from "@sdxc/mcp";

import toolset from "~/app/mcp/tools";

export default createTool(toolset.searchPosts, () => "Search posts");

The tool is declared with a description and an empty input schema, and mapped on the handler the initializer creates with createHandler:

--- a/app/mcp/tools/index.ts
+++ b/app/mcp/tools/index.ts
-import { tools } from "@sdxc/mcp";
+import { tools, tool } from "@sdxc/mcp";
+import * as s from "@sdxc/json-schema";

-export default tools({});
+export default tools({
+	searchPosts: tool("search_posts", {
+		description: "Search posts.",
+		input: s.object({}),
+	}),
+});
--- a/app/initializers/mcp.ts
+++ b/app/initializers/mcp.ts
-import { createHandler } from "@sdxc/mcp";
+import { createHandler, lazy } from "@sdxc/mcp";
+import toolset from "~/app/mcp/tools";
@@
+mcp.tools.map(toolset.searchPosts, lazy(() => import("~/app/mcp/tools/search-posts")));

A resource

--resource writes a handler that reads Markdown:

app/mcp/resources/article.ts
import { createResource } from "@sdxc/mcp";

import resourceset from "~/app/mcp/resources";

export default createResource(resourceset.article, {
	read: () => "# Article\n",
});

and declares it under a URI, with its name and media type:

app/mcp/resources/index.ts
export default resources({
	article: resource("app://article", {
		name: "Article",
		mimeType: "text/markdown",
	}),
});

The mapping is mcp.resources.map(resourceset.article, lazy(() => import(…))). The URI's scheme is your package.json name without its scope, so an app named @acme/blog declares blog://article; a name with nothing usable left takes app.

Names

Each name becomes three spellings:

sdxc make mcp posts/get-postValue
fileapp/mcp/tools/posts/get-post.ts
keytoolset.posts.getPost, in a posts group
tool nameposts_get_post, what an agent calls
resource URI, with --resource<scheme>://posts/get-post

The test

app/mcp/tools/search-posts.test.ts
import { describe, expect, test } from "vitest";

import handler from "~/app/mcp/tools/search-posts";

describe("search-posts", () => {
	test("exports the handler the MCP server maps", () => {
		expect(handler).toBeDefined();
	});

	test.todo("answers Search posts");
});
bun run test app/mcp/tools/search-posts.test.ts

Fill it in

  1. Describe the input in app/mcp/tools/index.ts, replacing s.object({}) with the arguments an agent sends, such as s.object({ query: s.string() }), and write a description that tells a model when to call the tool and what it gets back.

  2. Answer from the handler through what the server's context carries, such as your models, and return the text or structured content the description promises.

  3. A resource with params declares them in its URI, such as blog://posts/:slug, and reads them in read, returning null for one that does not exist.

Expose your app over MCP covers the server, its context and testing a tool through it, and @sdxc/mcp the whole API.

Where to go next

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.