textin-mcp-ts
Version:
TextIn MCP service in TypeScript
257 lines (256 loc) • 12.2 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fastmcp_1 = require("fastmcp");
const fs_1 = require("fs");
const fs_2 = require("fs");
const path_1 = require("path");
const axios_1 = __importDefault(require("axios"));
const zod_1 = require("zod");
// Zod schemas for parameter validation
const ConvertFileOptionsSchema = zod_1.z.object({
pdfPwd: zod_1.z.string().optional(),
charDetails: zod_1.z.boolean().optional(),
pageDetails: zod_1.z.boolean().optional(),
catalogDetails: zod_1.z.boolean().optional(),
dpi: zod_1.z.number().optional(),
pageStart: zod_1.z.number().optional(),
pageCount: zod_1.z.number().optional(),
getImage: zod_1.z.enum(['none', 'url', 'base64', 'both']).optional(),
getTable: zod_1.z.boolean().optional(),
getFormula: zod_1.z.boolean().optional()
});
const EnhanceImageOptionsSchema = zod_1.z.object({
enhanceMode: zod_1.z.number().optional(),
cropImage: zod_1.z.number().optional(),
onlyPosition: zod_1.z.number().optional(),
dewarpImage: zod_1.z.number().optional(),
deblurImage: zod_1.z.number().optional()
});
// Create an MCP server
const mcp = new fastmcp_1.FastMCP({
name: "TextIn MCP Server",
version: "1.0.0",
});
// Supported file formats
const supportedFormats = new Set([
'png', 'jpg', 'jpeg', 'pdf', 'bmp', 'tiff', 'webp',
'doc', 'docx', 'html', 'mhtml'
]);
// Helper function to validate file format
async function validateFile(filePath, formats) {
const ext = (0, path_1.extname)(filePath).toLowerCase().slice(1);
if (!formats.has(ext)) {
throw new Error(`Unsupported file format. Supported formats are: ${Array.from(formats).join(', ')}`);
}
try {
await fs_2.promises.access(filePath);
}
catch {
throw new Error(`Input file not found: ${filePath}`);
}
}
// Helper function to get API credentials
function getApiCredentials() {
const appId = process.env.TEXTIN_APP_ID;
const secretCode = process.env.TEXTIN_SECRET_CODE;
if (!appId || !secretCode) {
throw new Error('Missing API credentials. Please set TEXTIN_APP_ID and TEXTIN_SECRET_CODE environment variables.');
}
return { appId, secretCode };
}
// Convert file to markdown
mcp.addTool({
name: "convertFileToMarkdown",
description: "Convert various file formats to Markdown format, write to a Markdown file and return its content",
parameters: zod_1.z.object({
inputPath: zod_1.z.string().describe('Path to the input file (supported formats: png, jpg, jpeg, pdf, bmp, tiff, webp, doc, docx, html, mhtml)'),
outputPath: zod_1.z.string().optional().describe('Optional path to save the markdown file. If not provided, will use same name as input with .md extension'),
options: zod_1.z.object({
pdfPwd: zod_1.z.string().optional().describe('Optional password for encrypted PDF'),
charDetails: zod_1.z.boolean().optional().describe('Whether to return character position details'),
pageDetails: zod_1.z.boolean().optional().describe('Whether to return detailed page parsing results'),
catalogDetails: zod_1.z.boolean().optional().describe('Whether to return catalog details'),
dpi: zod_1.z.number().optional().describe('PDF document coordinate reference (72, 144, or 216)'),
pageStart: zod_1.z.number().optional().describe('Starting page number for PDF conversion'),
pageCount: zod_1.z.number().optional().describe('Number of pages to convert for PDF'),
getImage: zod_1.z.enum(['none', 'url', 'base64', 'both']).optional().describe('Image extraction mode (\'none\', \'url\', \'base64\', or \'both\')'),
getTable: zod_1.z.boolean().optional().describe('Whether to extract tables'),
getFormula: zod_1.z.boolean().optional().describe('Whether to extract formulas式')
}).optional()
}),
execute: async ({ inputPath, outputPath, options = {} }) => {
const validatedOptions = ConvertFileOptionsSchema.parse(options);
await validateFile(inputPath, supportedFormats);
const finalOutputPath = outputPath || inputPath.replace(/\.[^.]+$/, '.md');
const { appId, secretCode } = getApiCredentials();
const params = {
get_image: validatedOptions.getImage || 'both',
char_details: validatedOptions.charDetails ? 1 : 0,
page_details: validatedOptions.pageDetails ?? 1,
catalog_details: validatedOptions.catalogDetails ? 1 : 0,
dpi: validatedOptions.dpi || 144,
get_table: validatedOptions.getTable ?? 1,
get_formula: validatedOptions.getFormula ?? 1,
pdf_pwd: validatedOptions.pdfPwd,
page_start: validatedOptions.pageStart,
page_count: validatedOptions.pageCount
};
try {
const response = await axios_1.default.post('https://api.textin.com/ai/service/v1/pdf_to_markdown', (0, fs_1.createReadStream)(inputPath), {
headers: {
'x-ti-app-id': appId,
'x-ti-secret-code': secretCode
},
params
});
if (response.data.code !== 200) {
throw new Error(response.data.code);
}
const markdownContent = response.data.result?.markdown;
if (!markdownContent) {
throw new Error('No markdown content in response');
}
await fs_2.promises.writeFile(finalOutputPath, markdownContent, 'utf-8');
return markdownContent;
}
catch (error) {
throw new Error(`API request failed: ${error.message}`);
}
},
});
// Scan directory for supported files
mcp.addTool({
name: "scanDirectory",
description: "Scan directory for supported file formats",
parameters: zod_1.z.object({
directoryPath: zod_1.z.string().describe('Path to the directory to scan')
}),
execute: async ({ directoryPath }) => {
try {
const files = await fs_2.promises.readdir(directoryPath, { withFileTypes: true });
const supportedFiles = [];
for (const file of files) {
if (file.isFile()) {
const ext = (0, path_1.extname)(file.name).toLowerCase().slice(1);
if (supportedFormats.has(ext)) {
supportedFiles.push((0, path_1.join)(directoryPath, file.name));
}
}
}
return supportedFiles.join('\n');
}
catch (error) {
throw new Error(`Failed to scan directory: ${error.message}`);
}
},
});
// Extract information from documents
mcp.addTool({
name: "extractInfo",
description: "Extract information from various document formats",
parameters: zod_1.z.object({
inputPath: zod_1.z.string().describe('Path to the input file (supported formats: png, jpg, jpeg, doc, docx, pdf, ofd, xlsx, xls)'),
key: zod_1.z.string().optional().describe('Optional comma-separated list of keys to extract single values'),
tableHeader: zod_1.z.string().optional().describe('Optional comma-separated list of table headers to extract table data')
}),
execute: async ({ inputPath, key, tableHeader }) => {
const supportedExtractFormats = new Set([
'png', 'jpg', 'jpeg', 'doc', 'docx', 'pdf', 'ofd', 'xlsx', 'xls'
]);
await validateFile(inputPath, supportedExtractFormats);
const { appId, secretCode } = getApiCredentials();
try {
const response = await axios_1.default.post('https://api.textin.com/ai/service/v1/entity_extraction', (0, fs_1.createReadStream)(inputPath), {
headers: {
'x-ti-app-id': appId,
'x-ti-secret-code': secretCode
},
params: { key, table_header: tableHeader }
});
if (response.data.code !== 200) {
throw new Error(response.data.code);
}
const transformedResult = response.data.result.detail_structure.map((item) => {
return {
fields: Object.fromEntries(Object.entries(item.fields).map(([key, value]) => [
key,
value.map((fieldItem) => fieldItem.value)
])),
stamps: item.stamps.map((stamp) => {
const { position, ...rest } = stamp;
return rest;
})
};
});
return JSON.stringify(transformedResult);
}
catch (error) {
throw new Error(`API request failed: ${error.message}`);
}
},
});
// Enhance image
mcp.addTool({
name: "enhanceImage",
description: "Enhance and process images with various options",
parameters: zod_1.z.object({
inputPath: zod_1.z.string().describe('Path to the input image file or URL'),
outputPath: zod_1.z.string().optional().describe('Optional path to save the enhanced image. If not provided, will use same name with \'_enhanced\' suffix'),
options: zod_1.z.object({
enhanceMode: zod_1.z.number().optional().describe('Enhancement mode (-1: disabled, 1: brighten, 2: enhance and sharpen, 3: black&white, 4: grayscale, 5: remove shadow, 6: halftone)'),
cropImage: zod_1.z.number().optional().describe('Whether to crop image (0: no, 1: yes)'),
onlyPosition: zod_1.z.number().optional().describe('Whether to return only corner positions (0: return corners and image, 1: return only corners)'),
dewarpImage: zod_1.z.number().optional().describe('Whether to dewarp image (0: no, 1: yes)'),
deblurImage: zod_1.z.number().optional().describe('Whether to deblur image (0: no, 1: yes)')
}).optional()
}),
execute: async ({ inputPath, outputPath, options = {} }) => {
const validatedOptions = EnhanceImageOptionsSchema.parse(options);
const { appId, secretCode } = getApiCredentials();
try {
await fs_2.promises.access(inputPath);
}
catch {
throw new Error(`Input file not found: ${inputPath}`);
}
const finalOutputPath = outputPath || inputPath.replace(/\.[^.]+$/, '_enhanced' + (0, path_1.extname)(inputPath));
try {
const imageData = await fs_2.promises.readFile(inputPath);
const response = await axios_1.default.post('https://api.textin.com/ai/service/v1/crop_enhance_image', imageData, {
headers: {
'x-ti-app-id': appId,
'x-ti-secret-code': secretCode
},
params: {
enhance_mode: validatedOptions.enhanceMode ?? -1,
crop_image: validatedOptions.cropImage ?? 1,
only_position: validatedOptions.onlyPosition ?? 0,
dewarp_image: validatedOptions.dewarpImage ?? 1,
deblur_image: validatedOptions.deblurImage ?? 0
}
});
if (response.data.code !== 200) {
throw new Error(response.data);
}
const imageList = response.data.result?.image_list;
if (!imageList?.length) {
throw new Error('No enhanced image data received');
}
const imageBase64 = imageList[0].image;
const imageBuffer = Buffer.from(imageBase64.includes('base64,') ? imageBase64.split('base64,')[1] : imageBase64, 'base64');
await fs_2.promises.writeFile(finalOutputPath, imageBuffer);
return finalOutputPath;
}
catch (error) {
throw new Error(`Failed to enhance image: ${error.message}`);
}
},
});
// Start the server
mcp.start({
transportType: 'stdio',
});