# Tmcp

> Typescript SDK to build MCP servers in an agnostic way

- **Type:** MCP server
- **Install:** `agentstack add mcp-paoloricciuti-tmcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [paoloricciuti](https://agentstack.voostack.com/s/paoloricciuti)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [paoloricciuti](https://github.com/paoloricciuti)
- **Source:** https://github.com/paoloricciuti/tmcp
- **Website:** https://tmcp.io

## Install

```sh
agentstack add mcp-paoloricciuti-tmcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

> [!WARNING]
> Unfortunately i published the 1.0 by mistake...this package is currently under heavy development so there will be breaking changes in minors...threat this `1.x` as the `0.x` of any other package. Sorry for the disservice, every breaking will be properly labeled in the PR name.

# tmcp

A lightweight, schema-agnostic Model Context Protocol (MCP) server implementation with unified API design.

## Why tmcp?

tmcp offers significant advantages over the official MCP SDK:

- **🔄 Schema Agnostic**: Works with any validation library through adapters
- **📦 No Weird Dependencies**: Minimal footprint with only essential dependencies (looking at you `express`)
- **🎯 Unified API**: Consistent, intuitive interface across all MCP capabilities
- **🔌 Extensible**: Easy to add support for new schema libraries
- **⚡ Lightweight**: No bloat, just what you need

## Supported Schema Libraries

tmcp works with all major schema validation libraries through its adapter system:

- **Zod** - `@tmcp/adapter-zod`
- **Valibot** - `@tmcp/adapter-valibot`
- **ArkType** - `@tmcp/adapter-arktype`
- **Effect Schema** - `@tmcp/adapter-effect`
- **Zod v3** - `@tmcp/adapter-zod-v3`

## Installation

```bash
pnpm install tmcp
# Choose your preferred schema library adapter
pnpm install @tmcp/adapter-zod zod
# Choose your preferred transport
pnpm install @tmcp/transport-stdio  # For CLI/desktop apps
pnpm install @tmcp/transport-http   # For web-based clients
```

## Quick Start

### Standard I/O Transport (CLI/Desktop)

```javascript
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { StdioTransport } from '@tmcp/transport-stdio';
import { z } from 'zod';

const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(
	{
		name: 'my-server',
		version: '1.0.0',
		description: 'My awesome MCP server',
	},
	{
		adapter,
		capabilities: {
			tools: { listChanged: true },
			prompts: { listChanged: true },
			resources: { listChanged: true },
		},
	},
);

// While the adapter is optional (you can opt out by explicitly passing `adapter: undefined`) without an adapter the server cannot accept inputs, produce structured outputs, or request elicitations at all only do this for very simple servers.

// Define a tool with type-safe schema
server.tool(
	{
		name: 'calculate',
		description: 'Perform mathematical calculations',
		schema: z.object({
			operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
			a: z.number(),
			b: z.number(),
		}),
	},
	async ({ operation, a, b }) => {
		switch (operation) {
			case 'add':
				return a + b;
			case 'subtract':
				return a - b;
			case 'multiply':
				return a * b;
			case 'divide':
				return a / b;
		}
	},
);

// Start the server with stdio transport
const transport = new StdioTransport(server);
transport.listen();
```

### HTTP Transport (Web-based)

```javascript
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { HttpTransport } from '@tmcp/transport-http';
import { z } from 'zod';

const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(/* ... same server config ... */);

// Add tools as above...

// Create HTTP transport
const transport = new HttpTransport(server);

// Use with your preferred HTTP server (Bun example)
Bun.serve({
	port: 3000,
	async fetch(req) {
		const response = await transport.respond(req);
		if (response === null) {
			return new Response('Not Found', { status: 404 });
		}
		return response;
	},
});
```

## Defining Tools, Prompts, Resources, and Templates in Separate Files

For better code organization and reusability, you can define your tools, prompts, resources, and templates in separate files using the `defineTool`, `definePrompt`, `defineResource`, and `defineTemplate` utilities:

```ts
// tools/calculator.ts
import { defineTool } from 'tmcp/tool';
import { tool } from 'tmcp/utils';
import { z } from 'zod';

export const addTool = defineTool(
	{
		name: 'add',
		description: 'Add two numbers',
		schema: z.object({
			a: z.number(),
			b: z.number(),
		}),
	},
	async ({ a, b }) => tool.text(`${a} + ${b} = ${a + b}`),
);
```

```ts
// server.ts
import { McpServer } from 'tmcp';
import { addTool } from './tools/calculator.js';

const server = new McpServer(/* ... */);

// Register a single tool
server.tool(addTool);

// Or register multiple tools at once
server.tools([addTool, multiplyTool, divideTool]);
```

This approach enables:

- **Modular organization**: Keep related functionality in separate files
- **Reusability**: Share tool definitions across multiple servers
- **Better testing**: Test tool logic independently
- **Type safety**: Full TypeScript inference for parameters and return types

The same pattern works for `definePrompt`, `defineResource`, and `defineTemplate` with their corresponding `server.prompts()`, `server.resources()`, and `server.templates()` methods.

Adding the primitive to the server will error if the tool uses a different validation library than the one expressed in the adapter.

## API Reference

### McpServer

The main server class that handles MCP protocol communications.

#### Constructor

```javascript
new McpServer(serverInfo, options);
```

- `serverInfo`: Server metadata (name, version, description)
- `options`: Configuration object with optional adapter (for schema conversion) and capabilities

> [!IMPORTANT]
> While the adapter is optional (you can opt out by explicitly passing `adapter: undefined`) without an adapter the server cannot accept inputs, produce structured outputs, or request elicitations at all only do this for very simple servers.

#### Methods

##### `tool(definition, handler)` / `tools(definitions)`

Register one or more tools with optional schema validation.

```javascript
// Register a single tool inline
server.tool(
	{
		name: 'tool-name',
		description: 'Tool description',
		schema: yourSchema, // optional
	},
	async (input) => {
		// Tool implementation
		return { content: [{ type: 'text', text: 'Tool result' }] };
	},
);

// Register a tool created with defineTool
import { defineTool } from 'tmcp/tool';

const myTool = defineTool(
	{
		name: 'tool-name',
		description: 'Tool description',
		schema: yourSchema,
	},
	async (input) => {
		return { content: [{ type: 'text', text: 'Tool result' }] };
	},
);

server.tool(myTool);

// Register multiple tools at once
server.tools([tool1, tool2, tool3]);
```

##### `prompt(definition, handler)` / `prompts(definitions)`

Register one or more prompt templates with optional schema validation.

```javascript
// Register a single prompt inline
server.prompt(
  {
    name: 'prompt-name',
    description: 'Prompt description',
    schema: yourSchema, // optional
    complete: {
      paramName: (arg, context) => ({
        completion: {
          values: ['completion1', 'completion2'],
          total: 2,
          hasMore: false
        }
      })
    } // optional
  },
  async (input) => {
    // Prompt implementation
    return { messages: [...] };
  }
);

// Register a prompt created with definePrompt
import { definePrompt } from 'tmcp/prompt';

const myPrompt = definePrompt(
  {
    name: 'prompt-name',
    description: 'Prompt description',
    schema: yourSchema,
  },
  async (input) => {
    return { messages: [...] };
  }
);

server.prompt(myPrompt);

// Register multiple prompts at once
server.prompts([prompt1, prompt2, prompt3]);
```

##### `resource(definition, handler)` / `resources(definitions)`

Register one or more static resources.

```javascript
// Register a single resource inline
server.resource(
  {
    name: 'resource-name',
    description: 'Resource description',
    uri: 'file://path/to/resource'
  },
  async (uri, params) => {
    // Resource implementation
    return { contents: [...] };
  }
);

// Register a resource created with defineResource
import { defineResource } from 'tmcp/resource';

const myResource = defineResource(
  {
    name: 'resource-name',
    description: 'Resource description',
    uri: 'file://path/to/resource'
  },
  async (uri) => {
    return { contents: [...] };
  }
);

server.resource(myResource);

// Register multiple resources at once
server.resources([resource1, resource2, resource3]);
```

### Reducing Return Boilerplate with `tmcp/utils`

Most handlers end by returning some variant of `{ content: [...] }`, `{ messages: [...] }`, or `{ completion: { ... } }`. That shape is repetitive and easy to get wrong, especially when you also need to wire `isError` or `structuredContent`. The `tmcp/utils` entry point ships tiny factories that return the correct MCP payloads for you so handlers can stay focused on business logic.

```ts
import { tool, resource, prompt, complete } from 'tmcp/utils';

server.tool({ name: 'health-check', description: 'Ping' }, async () =>
	tool.text('ok'),
);

server.tool(
	{ name: 'profile-picture', description: 'My Profile Picture' },
	async () => tool.media('image', await loadPng(), 'image/png'),
);

server.resource(
	{ name: 'readme', description: 'Project README', uri: 'file://README.md' },
	async (uri) =>
		resource.text(uri, await readFile(uri, 'utf8'), 'text/markdown'),
);

server.prompt(
	{
		name: 'explain',
		description: '',
		schema: v.object({ topic: v.string() }),
	},
	async ({ topic }) => prompt.message(`Explain ${topic} like I am five.`),
);

server.template(
	{
		name: 'users',
		description: 'Template with completion',
		uri: 'users/{id}',
		complete: {
			id: async (arg) => complete.values(await findMatchingIds(arg)),
		},
	},
	async (uri) => resource.blob(uri, await fetchUserBlob(uri)),
);
```

you can also compose different kind of tools with `tool.mix`

```ts
tool.mix([
	tool.text('Indexed workspace'),
	tool.media('image', png, 'image/png'),
]);
```

however be aware that

1. you can't pass `tool.structured` to `tool.mix` (but you can pass a second argument that will be the structured content)
2. if you pass even one `tool.error` to the `tool.mix` the whole return value will be an error

```ts
const structuredContent = {
	cool: true,
};

tool.mix(
	[
		tool.text(JSON.stringify(structuredContent)),
		tool.media('image', png, 'image/png'),
	],
	structuredContent,
);
```

Each helper is fully typed and returns the correct MCP structure (`CallToolResult`, `ReadResourceResult`, `GetPromptResult`, or `CompleteResult`). That means you can still provide `structuredContent`, embed resources, or merge multiple results via `mix` without having to copy/paste the surrounding boilerplate.

##### `template(definition, handler)` / `templates(definitions)`

Register one or more URI templates for dynamic resources.

```javascript
// Register a single template inline
server.template(
  {
    name: 'template-name',
    description: 'Template description',
    uri: 'file://path/{id}/resource',
    complete: {
      id: (arg, context) => ({
        completion: {
          values: ['id1', 'id2', 'id3'],
          total: 3,
          hasMore: false
        }
      })
    } // optional
  },
  async (uri, params) => {
    // Template implementation using params.id
    return { contents: [...] };
  }
);

// Register a template created with defineTemplate
import { defineTemplate } from 'tmcp/template';

const myTemplate = defineTemplate(
  {
    name: 'template-name',
    description: 'Template description',
    uri: 'file://path/{id}/resource',
  },
  async (uri, params) => {
    return { contents: [...] };
  }
);

server.template(myTemplate);

// Register multiple templates at once
server.templates([template1, template2, template3]);
```

##### `receive(request, context?)`

Process an incoming MCP request with optional context information.

```javascript
const response = server.receive(jsonRpcRequest, {
	sessionId: 'session-123',
	auth: authInfo,
	sessionInfo: {
		clientCapabilities, // cached from the client's initialize call
		clientInfo, // client name/version metadata
		logLevel, // last log level requested by the client
	},
	custom: {
		userId: 'alice',
	},
});
```

> [!TIP]
> `sessionInfo` is automatically populated by the built-in transports. You can read it from handlers via `server.ctx.sessionInfo` to tailor behaviour to the connected client.

##### `request({ method, params })`

Send a raw JSON-RPC request to the connected client. This is useful for
calling experimental MCP APIs or any custom client method that does not yet
have a dedicated helper in `McpServer` or to send a request with a custom JSON-schema that is not expressible with your validation library.

```javascript
const result = await server.request({
	method: 'elicitation/create',
	params: {
		message: 'Provide deployment metadata',
		requestedSchema: {
			type: 'object',
			required: ['region', 'replicas', 'features'],
			properties: {
				region: {
					type: 'string',
					enum: ['us-east-1', 'us-west-2', 'eu-central-1'],
				},
				replicas: { type: 'integer', minimum: 1, maximum: 20 },
				features: {
					type: 'array',
					items: {
						type: 'string',
						enum: ['canary', 'observability', 'autoscaling'],
					},
					minItems: 1,
				},
			},
		},
	},
});
```

- `method`: Fully qualified MCP client method name
- `params` (optional): JSON-RPC params object/array accepted by that method

Handle the resolved payload like any other JSON-RPC response—cast or (better) validate
as needed when using this escape hatch.

## Advanced Examples

### Multiple Schema Libraries

```javascript
// Use different schemas for different tools
import { z } from 'zod';
import * as v from 'valibot';

server.tool(
	{
		name: 'zod-tool',
		schema: z.object({ name: z.string() }),
	},
	async ({ name }) => `Hello ${name}`,
);

server.tool(
	{
		name: 'valibot-tool',
		schema: v.object({ age: v.number() }),
	},
	async ({ age }) => `Age: ${age}`,
);
```

### Resource Templates with Completion

```javascript
server.template(
	{
		name: 'user-profile',
		description: 'Get user profile by ID',
		uri: 'users/{userId}/profile',
		complete: (arg, context) => {
			// Provide completions for userId parameter
			return ['user1', 'user2', 'user3'];
		},
	},
	async (uri, params) => {
		const user = await getUserById(params.userId);
		return {
			contents: [
				{
					uri,
					mimeType: 'application/json',
					text: JSON.stringify(user),
				},
			],
		};
	},
);
```

### Dynamic Properties with Getters

Sometimes you need properties that are computed dynamically at list-time rather than registration-time. For example, you might want to serve different descriptions based on which client is connected.

`tmcp` preserves JavaScript getters on the configuration object, allowing you to define properties that are evaluated each time the capability is listed:

```javascript
server.tool(
	{
		name: 'search',
		get description() {
			const client = server.ctx.sessionInfo?.clientInfo?.name;
			if (client === 'claude-code') {
				return 'Search the codebase for files, symbols, or text patterns';
			}
			return 'Search for information';
		},
	},
	() => {
		return { content: [{ type: 'text', text: 'Search results...' }] };
	},
);
```

The same pattern works for resources, templates, and prompts.

### Complex Validation

```javascript
const complexSchema = z.object({
	user: z.object({
		name: z.string().min(1),
		email: z.string().email(),
		age: z.number().min(18).max(120),
	}),
	preferences: z
		.object({
			theme: z.enum(['light', 'dark']),
			notifications: z.boolean(),
		})
		.optional(),
	tags: z.array(z.string()).default([]),
});

server.tool(
	{
		name: 'create-user',
		description: 'Create a new user with preferences',
		schema: complexSchema,
	},
	async (input) => {
		// Input is fully typed and validated
		const { user, preferences, tags } = input;
		return await createUser(user, preferences, tags);
	},
);
```

## Contributing

Contributions are welcome! Please see our [contributing guidelines](../../CONTRIBUTING.md) for details.

## Acknowledgments

Huge thanks to Sean O'Bannon that provided us with the `@tm

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [paoloricciuti](https://github.com/paoloricciuti)
- **Source:** [paoloricciuti/tmcp](https://github.com/paoloricciuti/tmcp)
- **License:** MIT
- **Homepage:** https://tmcp.io

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-paoloricciuti-tmcp
- Seller: https://agentstack.voostack.com/s/paoloricciuti
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
