[ 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
| Flag | Default | What it does |
|---|---|---|
--resource | write a resource in app/mcp/resources/ instead of a tool | |
--test, --no-test | make.tests | write the test beside it, or skip it |
--dry-run | print the files and the diffs, and change nothing |
A tool
The handler answers with text, for you to replace with what the tool returns:
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:
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:
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-post | Value |
|---|---|
| file | app/mcp/tools/posts/get-post.ts |
| key | toolset.posts.getPost, in a posts group |
| tool name | posts_get_post, what an agent calls |
resource URI, with --resource | <scheme>://posts/get-post |
The test
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
Describe the input in
app/mcp/tools/index.ts, replacings.object({})with the arguments an agent sends, such ass.object({ query: s.string() }), and write a description that tells a model when to call the tool and what it gets back.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.
A resource with params declares them in its URI, such as
blog://posts/:slug, and reads them inread, returningnullfor 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
Scaffold a resource —
--mcpwrites the list, get, create, update and delete tools for a model.Make a policy — the rule a tool asks before it answers.
Generate files — the flags, configuration and stubs every kind shares.