import {
TOOL_CREATE_DOCUMENT,
TOOL_CREATE_DOCUMENT_BLOCK,
TOOL_CREATE_DOCUMENT_PASSWORD,
TOOL_GET_DOCUMENT,
TOOL_GET_DOCUMENT_RAW,
TOOL_SET_DOCUMENT_PERMISSION,
} from '@/consts/index.js';
import { DocumentService } from '@/services/documents/document-service.js';
import { FeiShuApiError } from '@/services/error.js';
/**
* Document Tools
*
* Defines MCP tools for FeiShu document operations.
*/
import { z } from 'zod';
import type { ToolRegistryParams } from './index.js';
/**
* Register document tools with the MCP server
*
* @param params - Tool registration parameters
*/
export function registerDocumentTools(params: ToolRegistryParams): void {
const { server, services, logger } = params;
// Get document raw content
server.tool(
TOOL_GET_DOCUMENT_RAW,
'Retrieve raw content from a FeiShu document by ID',
{
docId: z
.string()
.describe(
'The document ID of the FeiShu file to fetch, typically found in a URL like feishu.cn/wiki/<documentId>...',
),
},
async ({ docId }) => {
try {
logger.info(`Reading FeiShu document ${docId}`);
const content = await services.documents.getDocumentContent(docId);
return {
content: [{ type: 'text' as const, text: content }],
};
} catch (error) {
// Create appropriate error message based on error type
let errorMessage: string;
if (error instanceof FeiShuApiError) {
// Handle specific FeiShu API errors
logger.error(
`FeiShu API Error (${error.code || 'unknown'}): ${error.message}`,
);
errorMessage = `FeiShu API Error: ${error.message}`;
} else {
// Handle generic errors
logger.error(`Failed to fetch document ${docId}:`, error);
errorMessage = `Error fetching document: ${error instanceof Error ? error.message : String(error)}`;
}
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
// Get document metadata
server.tool(
TOOL_GET_DOCUMENT,
'Retrieve metadata for a FeiShu document',
{
docId: z.string().describe('The document ID to get information about'),
},
async ({ docId }) => {
try {
logger.info(`Getting metadata for document ${docId}`);
const info = await services.documents.getDocumentInfo(docId);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(info, null, 2) },
],
};
} catch (error) {
const errorMessage =
error instanceof FeiShuApiError
? `FeiShu API Error: ${error.message}`
: `Error retrieving document info: ${error}`;
logger.error(errorMessage);
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
// Create a new document
server.tool(
TOOL_CREATE_DOCUMENT,
'Create a new FeiShu document with optional title and folder location',
{
title: z
.string()
.optional()
.describe('Title of the document to create'),
folderToken: z
.string()
.optional()
.describe(
'Folder token to create the document in (optional, creates in root if not specified)',
),
},
async ({ title, folderToken }) => {
try {
logger.info(`Creating FeiShu document: ${title || 'Untitled'}`);
const result = await services.documents.createDocument(title, folderToken);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(result, null, 2) },
],
};
} catch (error) {
const errorMessage =
error instanceof FeiShuApiError
? `FeiShu API Error: ${error.message}`
: `Error creating document: ${error}`;
logger.error(errorMessage);
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
// Create blocks in a document
server.tool(
TOOL_CREATE_DOCUMENT_BLOCK,
'Create content blocks in a FeiShu document. Supports text, headings, lists, code, quotes, todos, and dividers.',
{
documentId: z
.string()
.describe('The document ID to add blocks to'),
blockId: z
.string()
.optional()
.describe(
'Parent block ID to add children to (defaults to document root if not specified)',
),
blocks: z
.array(
z.object({
type: z
.enum([
'text',
'heading1',
'heading2',
'heading3',
'heading4',
'heading5',
'heading6',
'bullet',
'ordered',
'code',
'quote',
'todo',
'divider',
])
.describe('Type of block to create'),
content: z
.string()
.optional()
.describe('Text content for the block (not needed for divider)'),
done: z
.boolean()
.optional()
.describe('For todo blocks: whether the task is done'),
language: z
.number()
.optional()
.describe('For code blocks: language identifier (1=PlainText, 2=ABAP, etc.)'),
}),
)
.describe('Array of blocks to create'),
index: z
.number()
.optional()
.describe('Index to insert blocks at (-1 or omit for end)'),
},
async ({ documentId, blockId, blocks, index }) => {
try {
logger.info(
`Creating ${blocks.length} blocks in document ${documentId}`,
);
const blockRequests = blocks.map((block) => {
switch (block.type) {
case 'text':
return DocumentService.createTextBlock(block.content || '');
case 'heading1':
return DocumentService.createHeadingBlock(block.content || '', 1);
case 'heading2':
return DocumentService.createHeadingBlock(block.content || '', 2);
case 'heading3':
return DocumentService.createHeadingBlock(block.content || '', 3);
case 'heading4':
return DocumentService.createHeadingBlock(block.content || '', 4);
case 'heading5':
return DocumentService.createHeadingBlock(block.content || '', 5);
case 'heading6':
return DocumentService.createHeadingBlock(block.content || '', 6);
case 'bullet':
return DocumentService.createBulletBlock(block.content || '');
case 'ordered':
return DocumentService.createOrderedBlock(block.content || '');
case 'code':
return DocumentService.createCodeBlock(
block.content || '',
block.language || 1,
);
case 'quote':
return DocumentService.createQuoteBlock(block.content || '');
case 'todo':
return DocumentService.createTodoBlock(
block.content || '',
block.done || false,
);
case 'divider':
return DocumentService.createDividerBlock();
default:
return DocumentService.createTextBlock(block.content || '');
}
});
const result = await services.documents.createBlocks(
documentId,
blockId || documentId,
blockRequests,
index ?? -1,
);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(result, null, 2) },
],
};
} catch (error) {
const errorMessage =
error instanceof FeiShuApiError
? `FeiShu API Error: ${error.message}`
: `Error creating blocks: ${error}`;
logger.error(errorMessage);
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
// Set document permission
server.tool(
TOOL_SET_DOCUMENT_PERMISSION,
'Set public sharing permission for a FeiShu document. Use this to make a document accessible via link.',
{
documentId: z
.string()
.describe('The document ID to set permission for'),
linkShareEntity: z
.enum([
'tenant_readable',
'tenant_editable',
'anyone_readable',
'anyone_editable',
'closed',
])
.default('anyone_editable')
.describe(
'Link sharing permission: tenant_readable (org can view), tenant_editable (org can edit), anyone_readable (anyone can view), anyone_editable (anyone can edit), closed (disabled)',
),
},
async ({ documentId, linkShareEntity }) => {
try {
logger.info(
`Setting permission for document ${documentId} to ${linkShareEntity}`,
);
const result = await services.documents.setPublicSharing(
documentId,
linkShareEntity,
);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(result, null, 2) },
],
};
} catch (error) {
const errorMessage =
error instanceof FeiShuApiError
? `FeiShu API Error: ${error.message}`
: `Error setting permission: ${error}`;
logger.error(errorMessage);
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
// Create document password
server.tool(
TOOL_CREATE_DOCUMENT_PASSWORD,
'Create a password for document sharing. The document must have link sharing enabled first.',
{
documentId: z
.string()
.describe('The document ID to create password for'),
},
async ({ documentId }) => {
try {
logger.info(`Creating password for document ${documentId}`);
const result = await services.documents.createSharePassword(documentId);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
password: result.password,
message: `Password created. Share link: https://feishu.cn/docx/${documentId}`,
},
null,
2,
),
},
],
};
} catch (error) {
const errorMessage =
error instanceof FeiShuApiError
? `FeiShu API Error: ${error.message}`
: `Error creating password: ${error}`;
logger.error(errorMessage);
return {
content: [{ type: 'text' as const, text: errorMessage }],
};
}
},
);
}