tana-mcp-server.ts•26.1 kB
/**
* Tana MCP Server
* An MCP server that connects to the Tana Input API
*/
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { TanaClient } from './tana-client';
import {
TanaBooleanNode,
TanaDateNode,
TanaFileNode,
TanaPlainNode,
TanaReferenceNode,
TanaSupertag,
TanaUrlNode
} from '../types/tana-api';
// Define Zod schemas for validating inputs
const SupertagSchema = z.object({
id: z.string(),
fields: z.record(z.string()).optional()
});
// We need a recursive type for NodeSchema to handle field nodes and other nested structures
const NodeSchema = z.lazy(() =>
z.object({
name: z.string().optional(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional(),
children: z.array(z.any()).optional(), // Will be validated by implementation
// Fields for specific node types
dataType: z.enum(['plain', 'reference', 'date', 'url', 'boolean', 'file']).optional(),
id: z.string().optional(), // For reference nodes
value: z.boolean().optional(), // For boolean nodes
file: z.string().optional(), // For file nodes (base64)
filename: z.string().optional(), // For file nodes
contentType: z.string().optional(), // For file nodes
// Field node properties
type: z.literal('field').optional(),
attributeId: z.string().optional()
})
);
export class TanaMcpServer {
private readonly server: McpServer;
private readonly tanaClient: TanaClient;
constructor(apiToken: string, endpoint?: string) {
// Create the Tana client
this.tanaClient = new TanaClient({
apiToken,
endpoint
});
// Create the MCP server with proper capabilities
this.server = new McpServer({
name: 'Tana MCP Server',
version: '1.2.0'
});
// Register tools
this.registerTools();
// Register prompts
this.registerPrompts();
// Register resources
this.registerResources();
}
/**
* Start the MCP server
*/
async start(): Promise<void> {
// Create the transport
const transport = new StdioServerTransport();
// Connect the server to the transport
await this.server.connect(transport);
// Server is ready - no logging to stderr to avoid protocol interference
}
/**
* Register tools for interacting with Tana
*/
private registerTools(): void {
// Create a plain node tool
this.server.tool(
'create_plain_node',
{
targetNodeId: z.string().optional(),
name: z.string(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional()
},
async ({ targetNodeId, name, description, supertags }) => {
try {
const node: TanaPlainNode = {
name,
description,
supertags
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating plain node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a reference node tool
this.server.tool(
'create_reference_node',
{
targetNodeId: z.string().optional(),
referenceId: z.string()
},
async ({ targetNodeId, referenceId }) => {
try {
const node: TanaReferenceNode = {
dataType: 'reference',
id: referenceId
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating reference node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a date node tool
this.server.tool(
'create_date_node',
{
targetNodeId: z.string().optional(),
date: z.string(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional()
},
async ({ targetNodeId, date, description, supertags }) => {
try {
const node: TanaDateNode = {
dataType: 'date',
name: date,
description,
supertags
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating date node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a URL node tool
this.server.tool(
'create_url_node',
{
targetNodeId: z.string().optional(),
url: z.string().url(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional()
},
async ({ targetNodeId, url, description, supertags }) => {
try {
const node: TanaUrlNode = {
dataType: 'url',
name: url,
description,
supertags
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating URL node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a checkbox node tool
this.server.tool(
'create_checkbox_node',
{
targetNodeId: z.string().optional(),
name: z.string(),
checked: z.boolean(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional()
},
async ({ targetNodeId, name, checked, description, supertags }) => {
try {
const node: TanaBooleanNode = {
dataType: 'boolean',
name,
value: checked,
description,
supertags
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating checkbox node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a file node tool
this.server.tool(
'create_file_node',
{
targetNodeId: z.string().optional(),
fileData: z.string(), // base64 encoded file data
filename: z.string(),
contentType: z.string(),
description: z.string().optional(),
supertags: z.array(SupertagSchema).optional()
},
async ({ targetNodeId, fileData, filename, contentType, description, supertags }) => {
try {
const node: TanaFileNode = {
dataType: 'file',
file: fileData,
filename,
contentType,
description,
supertags
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating file node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a field node tool
this.server.tool(
'create_field_node',
{
targetNodeId: z.string().optional(),
attributeId: z.string(),
children: z.array(NodeSchema).optional()
},
async ({ targetNodeId, attributeId, children }) => {
try {
// Properly type the field node according to TanaFieldNode interface
const fieldNode = {
type: 'field' as const, // Use 'as const' to ensure type is "field"
attributeId,
children
};
// Cast to TanaNode to satisfy the type system
const result = await this.tanaClient.createNode(targetNodeId, fieldNode as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating field node: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Set node name tool
this.server.tool(
'set_node_name',
{
nodeId: z.string(),
newName: z.string()
},
async ({ nodeId, newName }) => {
try {
const result = await this.tanaClient.setNodeName(nodeId, newName);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error setting node name: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a complex node structure tool
this.server.tool(
'create_node_structure',
{
targetNodeId: z.string().optional(),
node: NodeSchema
},
async ({ targetNodeId, node }) => {
try {
// Cast to TanaNode to satisfy the type system
const result = await this.tanaClient.createNode(targetNodeId, node as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating node structure: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a supertag tool
this.server.tool(
'create_supertag',
{
targetNodeId: z.string().optional().default('SCHEMA'),
name: z.string(),
description: z.string().optional()
},
async ({ targetNodeId, name, description }) => {
try {
const node: TanaPlainNode = {
name,
description,
supertags: [{ id: 'SYS_T01' }]
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating supertag: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Create a field tool
this.server.tool(
'create_field',
{
targetNodeId: z.string().optional().default('SCHEMA'),
name: z.string(),
description: z.string().optional()
},
async ({ targetNodeId, name, description }) => {
try {
const node: TanaPlainNode = {
name,
description,
supertags: [{ id: 'SYS_T02' }]
};
const result = await this.tanaClient.createNode(targetNodeId, node);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
isError: false
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating field: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
}
/**
* Register prompts for common Tana operations
*/
private registerPrompts(): void {
// Prompt for creating a task
this.server.prompt(
'create-task',
'Create a task node in Tana with optional due date and tags',
{
title: z.string().describe('Task title'),
description: z.string().optional().describe('Task description'),
dueDate: z.string().optional().describe('Due date in ISO format (YYYY-MM-DD)'),
priority: z.enum(['high', 'medium', 'low']).optional().describe('Task priority'),
tags: z.string().optional().describe('Comma-separated tags to apply to the task')
},
({ title, description, dueDate, priority, tags }) => {
const parts = [`Create a task in Tana: "${title}"`];
if (description) parts.push(`Description: ${description}`);
if (dueDate) parts.push(`Due date: ${dueDate}`);
if (priority) parts.push(`Priority: ${priority}`);
if (tags) parts.push(`Tags: ${tags}`);
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: parts.join('\n')
}
}]
};
}
);
// Prompt for creating a project
this.server.prompt(
'create-project',
'Create a project structure in Tana with goals and milestones',
{
name: z.string().describe('Project name'),
description: z.string().optional().describe('Project description'),
goals: z.string().optional().describe('Comma-separated list of project goals'),
startDate: z.string().optional().describe('Start date in ISO format'),
endDate: z.string().optional().describe('End date in ISO format'),
team: z.string().optional().describe('Comma-separated team member names')
},
({ name, description, goals, startDate, endDate, team }) => {
const parts = [`Create a project in Tana: "${name}"`];
if (description) parts.push(`Description: ${description}`);
if (goals) {
parts.push('Goals:');
const goalList = goals.split(',').map(g => g.trim());
goalList.forEach(goal => parts.push(`- ${goal}`));
}
if (startDate) parts.push(`Start date: ${startDate}`);
if (endDate) parts.push(`End date: ${endDate}`);
if (team) parts.push(`Team: ${team}`);
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: parts.join('\n')
}
}]
};
}
);
// Prompt for creating meeting notes
this.server.prompt(
'create-meeting-notes',
'Create structured meeting notes in Tana',
{
title: z.string().describe('Meeting title'),
date: z.string().describe('Meeting date in ISO format'),
attendees: z.string().describe('Comma-separated list of attendees'),
agenda: z.string().optional().describe('Comma-separated meeting agenda items'),
notes: z.string().optional().describe('Meeting notes'),
actionItems: z.string().optional().describe('Action items as JSON string array with task, assignee, and dueDate fields')
},
({ title, date, attendees, agenda, notes, actionItems }) => {
const parts = [
`Create meeting notes in Tana:`,
`Title: ${title}`,
`Date: ${date}`,
`Attendees: ${attendees}`
];
if (agenda) {
parts.push('\nAgenda:');
const agendaItems = agenda.split(',').map(a => a.trim());
agendaItems.forEach(item => parts.push(`- ${item}`));
}
if (notes) {
parts.push(`\nNotes:\n${notes}`);
}
if (actionItems) {
parts.push('\nAction Items:');
try {
const items = JSON.parse(actionItems);
if (Array.isArray(items)) {
items.forEach((item: any) => {
let actionText = `- ${item.task}`;
if (item.assignee) actionText += ` (assigned to: ${item.assignee})`;
if (item.dueDate) actionText += ` [due: ${item.dueDate}]`;
parts.push(actionText);
});
}
} catch (e) {
parts.push(`- ${actionItems}`);
}
}
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: parts.join('\n')
}
}]
};
}
);
// Prompt for knowledge base entry
this.server.prompt(
'create-knowledge-entry',
'Create a knowledge base entry in Tana',
{
topic: z.string().describe('Topic or title'),
category: z.string().optional().describe('Category or type'),
content: z.string().describe('Main content'),
sources: z.string().optional().describe('Comma-separated reference sources or links'),
relatedTopics: z.string().optional().describe('Comma-separated related topics for linking')
},
({ topic, category, content, sources, relatedTopics }) => {
const parts = [`Create a knowledge entry in Tana about: "${topic}"`];
if (category) parts.push(`Category: ${category}`);
parts.push(`\nContent:\n${content}`);
if (sources) {
parts.push('\nSources:');
const sourceList = sources.split(',').map(s => s.trim());
sourceList.forEach(source => parts.push(`- ${source}`));
}
if (relatedTopics) {
parts.push(`\nRelated topics: ${relatedTopics}`);
}
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: parts.join('\n')
}
}]
};
}
);
}
/**
* Register resources for interacting with Tana
*/
private registerResources(): void {
// API documentation resource
this.server.resource(
'api-docs',
'tana://api/documentation',
{
name: 'Tana API Documentation',
description: 'Overview of Tana Input API capabilities and usage',
mimeType: 'text/markdown'
},
async () => ({
contents: [{
uri: 'tana://api/documentation',
mimeType: 'text/markdown',
text: `# Tana Input API Documentation
## Overview
This MCP server provides access to Tana's Input API, allowing you to create and manipulate nodes in your Tana workspace.
## Available Node Types
- **Plain nodes**: Basic text nodes with optional formatting
- **Reference nodes**: Links to existing nodes by ID
- **Date nodes**: Nodes representing dates
- **URL nodes**: Web links with metadata
- **Checkbox nodes**: Boolean/task nodes
- **File nodes**: Attachments with base64 encoded data
- **Field nodes**: Structured data fields
## Tools Available
### Basic Node Creation
- \`create_plain_node\`: Create a simple text node
- \`create_reference_node\`: Create a reference to another node
- \`create_date_node\`: Create a date node
- \`create_url_node\`: Create a URL node
- \`create_checkbox_node\`: Create a checkbox/task node
- \`create_file_node\`: Create a file attachment node
### Advanced Features
- \`create_field_node\`: Create structured field nodes
- \`create_node_structure\`: Create complex nested node structures
- \`set_node_name\`: Update the name of an existing node
### Schema Management
- \`create_supertag\`: Create new supertags (node types)
- \`create_field\`: Create new field definitions
## API Limits
- Maximum 100 nodes per request
- 1 request per second per token
- Payload limit: 5000 characters
- Workspace limit: 750k nodes`
}]
})
);
// Node types reference
this.server.resource(
'node-types',
'tana://reference/node-types',
{
name: 'Tana Node Types Reference',
description: 'Detailed information about all supported node types',
mimeType: 'text/markdown'
},
async () => ({
contents: [{
uri: 'tana://reference/node-types',
mimeType: 'text/markdown',
text: `# Tana Node Types Reference
## Plain Node
The most basic node type, used for text content.
\`\`\`json
{
"name": "Node title",
"description": "Node content with **formatting**",
"supertags": [{"id": "tagId"}],
"children": []
}
\`\`\`
## Reference Node
Links to an existing node.
\`\`\`json
{
"dataType": "reference",
"id": "targetNodeId"
}
\`\`\`
## Date Node
Represents a date value.
\`\`\`json
{
"dataType": "date",
"name": "2024-01-01",
"description": "Optional description"
}
\`\`\`
## URL Node
Stores web links.
\`\`\`json
{
"dataType": "url",
"name": "https://example.com",
"description": "Link description"
}
\`\`\`
## Checkbox Node
Boolean/task nodes.
\`\`\`json
{
"dataType": "boolean",
"name": "Task name",
"value": false
}
\`\`\`
## File Node
File attachments.
\`\`\`json
{
"dataType": "file",
"file": "base64EncodedData",
"filename": "document.pdf",
"contentType": "application/pdf"
}
\`\`\`
## Field Node
Structured data fields.
\`\`\`json
{
"type": "field",
"attributeId": "fieldId",
"children": [/* nested nodes */]
}
\`\`\``
}]
})
);
// Examples resource
this.server.resource(
'examples',
'tana://examples/common-patterns',
{
name: 'Common Usage Examples',
description: 'Example patterns for common Tana operations',
mimeType: 'text/markdown'
},
async () => ({
contents: [{
uri: 'tana://examples/common-patterns',
mimeType: 'text/markdown',
text: `# Common Tana Usage Examples
## Creating a Task with Due Date
\`\`\`javascript
// Use create_checkbox_node with a child date node
{
"name": "Complete project proposal",
"checked": false,
"children": [
{
"type": "field",
"attributeId": "SYS_A13", // Due date field
"children": [{
"dataType": "date",
"name": "2024-12-31"
}]
}
]
}
\`\`\`
## Creating a Project Structure
\`\`\`javascript
// Use create_node_structure for complex hierarchies
{
"name": "Q1 2024 Product Launch",
"supertags": [{"id": "projectTagId"}],
"children": [
{
"name": "Milestones",
"children": [
{"name": "Design Complete", "dataType": "date", "name": "2024-01-15"},
{"name": "Development Done", "dataType": "date", "name": "2024-02-28"}
]
},
{
"name": "Tasks",
"children": [
{"dataType": "boolean", "name": "Create mockups", "value": false},
{"dataType": "boolean", "name": "Write documentation", "value": false}
]
}
]
}
\`\`\`
## Adding Multiple Tags
\`\`\`javascript
{
"name": "Important Note",
"supertags": [
{"id": "tag1Id"},
{"id": "tag2Id", "fields": {"priority": "high"}}
]
}
\`\`\``
}]
})
);
// Server info resource
this.server.resource(
'server-info',
'tana://info',
{
name: 'Server Information',
description: 'Current server status and configuration',
mimeType: 'text/plain'
},
async () => ({
contents: [{
uri: 'tana://info',
mimeType: 'text/plain',
text: `Tana MCP Server v1.2.0
Status: Connected
API Endpoint: ${this.tanaClient['endpoint']}
Capabilities:
- Tools: Multiple tools available for node creation and management
- Prompts: Pre-configured prompts for common tasks
- Resources: Documentation and examples available
For detailed API documentation, see the 'api-docs' resource.
For examples, see the 'examples' resource.`
}]
})
);
}
}