mcp-thing
v0.2.1
Published
Reflection-based CLI to expose TypeScript classes as MCP tools over stdio.
Maintainers
Readme
mcp-thing
Expose TypeScript classes as MCP (Model Context Protocol) tools over stdio.
mcp-thing is the fast, easy way to create a fully working MCP service: write
TypeScript classes, export them, and point the CLI at one service file or a
directory of services. Public methods become MCP tools, JSDoc becomes tool
documentation, and TypeScript types become MCP JSON schemas.
Requirements
- Node.js 18+
- A TypeScript service file or services directory
npm installin the service source directory if your services import dependencies
Quick Start With npx
Create a service:
// services/EchoService.ts
export class EchoService {
/**
* Returns the provided text.
* @param text Text to return.
*/
public echo(text: string): string {
return text;
}
}Run it:
npx mcp-thing --run ./services/EchoService.tsThe exported tool is named EchoService_echo.
You can also use the short binary name:
npx -p mcp-thing mcpt --run ./servicesInstall
Install globally if you want the mcpt command available everywhere:
npm install -g mcp-thingRun a service file or services directory:
mcpt --run ./services/EchoService.ts
mcpt --run ./servicesYou can omit --run and pass the source directly:
mcpt ./services/EchoService.ts
mcpt ./servicesCompile
Compile a TypeScript service file or services directory into a standalone JavaScript MCP server. For best startup performance in MCP clients, compile your service and run the generated JavaScript entrypoint instead of parsing TypeScript on every launch:
mcpt --compile ./services/EchoService.ts
mcpt --compile ./servicesBy default this writes to dist inside the source directory, or the parent
directory when the source is a file. You can provide a custom output directory:
mcpt --compile ./services/EchoService.ts ./build/mcp
mcpt --compile ./services ./build/mcpRun the compiled server:
cd ./services/dist
npm install
node index.jsCompiled output includes:
index.js— generated MCP server entrypointschema-manifest.json— discovered services, tools, schemas, and result modes- emitted service
.jsfiles package.jsonwith the runtime dependency onmcp-thing
CLI Options
--server-name <name>— override MCPserverInfo.name--server-version <version>— override MCPserverInfo.version--result-mode <structured|content>— choose global successful result format--verbose— enable detailed stderr logs--silent— suppress non-error stderr logs, including startup notice
The default result mode is structured.
MCP Client Config
Most MCP clients launch stdio servers with a command and args.
With npx:
{
"mcpServers": {
"my-tools": {
"command": "npx",
"args": ["mcp-thing", "--run", "/absolute/path/to/services"]
}
}
}With a global install:
{
"mcpServers": {
"my-tools": {
"command": "mcpt",
"args": ["--run", "/absolute/path/to/services"]
}
}
}With compiled output:
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["/absolute/path/to/services/dist/index.js"]
}
}
}Use --run while iterating locally. Use compiled output when you want a faster,
plain JavaScript startup path for MCP clients.
Tool Discovery
Tools are discovered from exported TypeScript classes:
- Exported classes are services.
- Public methods are tools.
- Private methods and constructors are ignored.
- Classes with no exposed methods are skipped.
- Use
@mcpIgnoreto skip an exported helper class or a public method.
/**
* @mcpIgnore
*/
export class HelperError extends Error {}
export class PackageService {
/**
* @mcpIgnore
*/
public internalDebug(): string {
return 'debug';
}
}Tool metadata comes from names and JSDoc:
- Default tool name:
<ClassName>_<methodName> @mcpToolName <name>overrides the tool name- Method JSDoc description becomes the MCP tool description
@paramdescriptions become parameter descriptions
export class GreetingService {
/**
* Greets a user.
* @mcpToolName hello
* @param name User name.
*/
public greet(name: string): string {
return `Hello, ${name}!`;
}
}Input Schemas
mcp-thing infers MCP input schemas from TypeScript method parameters.
Supported inference includes:
string,number,boolean- optional parameters like
count?: number - optional/nullish unions like
number | undefinedandstring | null - arrays like
string[]andArray<number> - literal unions as enums, like
'name' | 'description' - named type aliases for simple supported types
- TypeScript enums and const enums with string or number values
- plain object, type alias, and interface shapes with simple properties
- recursive array item schemas, including enum arrays
Unsupported complex types degrade to a simpler schema and produce a warning instead of silently pretending the schema is precise.
export class SearchService {
/**
* Searches packages.
* @param keyword Search term.
* @param by Search field.
* @param count Maximum number of results.
*/
public search(
keyword: string,
by: 'name' | 'description' = 'name',
count?: number,
) {
return { packages: [], total: 0 };
}
}This exposes by as a string enum and count as an optional number.
Named simple types work too:
type SearchBy = 'name' | 'description' | 'maintainer';
enum SortBy {
Name = 'name',
Votes = 'votes',
}
interface SearchOptions {
keyword: string;
by: SearchBy;
sort: SortBy;
count?: number;
}
export class SearchService {
public search(options: SearchOptions) {
return { packages: [], total: 0 };
}
}This exposes options as an object schema, by and sort as enums, and
count as an optional number.
Intentionally unsupported complex TypeScript features include generics, mapped
types, conditional types, recursive types, inheritance-heavy classes, and
utility types like Partial<T>, Pick<T>, and Record<K, V>.
Optional Schema Constraints
You can enrich inferred input schemas with optional JSDoc tags. These tags are not required; use them only when clients should discover validation details.
Supported parameter constraint tags:
@minLength <param> <number>@maxLength <param> <number>@minimum <param> <number>@maximum <param> <number>@exclusiveMinimum <param> <number>@exclusiveMaximum <param> <number>@default <param> <value>@minItems <param> <number>@maxItems <param> <number>@pattern <param> <regex>@format <param> <format>
Example:
export class AurService {
/**
* Searches AUR packages.
* @param keyword Search term.
* @minLength keyword 2
* @param by Search field.
* @default by name
* @param count Maximum result count.
* @minimum count 1
* @maximum count 500
* @default count 50
*/
public search(
keyword: string,
by: 'name' | 'description' | 'maintainer' = 'name',
count?: number,
) {
return { packages: [], total: 0 };
}
}Clients will see keyword.minLength, by.enum, by.default, count.minimum,
count.maximum, and count.default in the input schema.
Output Schemas
mcp-thing also infers output schemas from simple return types, including
plain object aliases and interfaces.
public packageInfo(name: string): { name: string; version: string } {
return { name, version: '1.0.0' };
}This emits an MCP outputSchema for:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
},
"required": ["name", "version"]
}Primitive and array returns are wrapped as { "result": value } in structured
responses, so their output schemas are wrapped the same way.
public names(): string[] {
return ['yay', 'paru'];
}Produces structured content shaped like:
{
"result": ["yay", "paru"]
}Complex return types may still be returned as structured data at runtime, but
outputSchema is only emitted when the shape is simple enough to infer.
Tool Results
Successful tool calls default to structured MCP results:
public info(name: string) {
return { name, version: '1.0.0' };
}Default MCP result:
{
"content": [],
"structuredContent": {
"name": "yay",
"version": "1.0.0"
}
}This avoids duplicating JSON into both text content and structured content.
content is still present because MCP requires it to be an array.
Return behavior in structured mode:
- plain objects become
structuredContentas-is - strings, numbers, booleans, arrays, and null are wrapped as
{ "result": value } undefinedbecomes empty structured content{}- thrown errors become
{ isError: true, content: [{ type: "text", text: message }] }
Content Mode
Use content mode when a client only reads regular text content.
Globally:
mcpt --run ./services --result-mode contentPer tool:
export class ExplainService {
/**
* Explains a package.
* @mcpResultMode content
*/
public explain(name: string): string {
return `Package ${name} is maintained by ...`;
}
}In content mode, structuredContent is omitted and non-string values are
serialized as JSON text.
Explicit ToolResult
Most methods should return normal values. For advanced cases, a method can
return a full MCP-style tool result to control content, structuredContent,
and isError directly.
import type { ToolResult } from 'mcp-thing';
export class ImportService {
public importPackages(): ToolResult {
return {
isError: true,
content: [{ type: 'text', text: 'Imported 8 packages, failed 2.' }],
structuredContent: {
imported: 8,
failed: ['pkg-a', 'pkg-b'],
},
};
}
}This is optional and mainly useful for partial success or custom error formatting. For normal failures, throw an error.
Server Identity
Set serverInfo.name and serverInfo.version with class-level JSDoc:
/**
* @mcpServerName My MCP Server
* @mcpServerVersion 1.2.3
*/
export class MyService {
public ping(): string {
return 'pong';
}
}CLI flags take precedence over JSDoc values.
Examples
See examples/CalculatorService.ts for a small service with server identity,
parameter descriptions, and a custom tool name.
