textlint
Version:
The pluggable linting tool for natural language.
300 lines (270 loc) • 11.4 kB
text/typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createLinter, loadTextlintrc, type CreateLinterOptions } from "../index.js";
import { existsSync } from "node:fs";
import { TextlintMessageSchema } from "./schemas.js";
import { TextlintKernelDescriptor } from "@textlint/kernel";
// Define error types as a union type
type TextlintMcpErrorType = "lintFile_error" | "lintText_error" | "fixFiles_error" | "fixText_error";
// Define MCP server options type as specified in the issue
export type McpServerOptions = {
configFilePath?: string; // Config file path
node_modulesDir?: string; // Custom node_modules directory
ignoreFilePath?: string; // .textlintignore file path
quiet?: boolean; // Report errors only
cwd?: string; // Current working directory
descriptor?: TextlintKernelDescriptor; // Direct configuration
};
const makeLinterOptions = async (options: McpServerOptions = {}): Promise<CreateLinterOptions> => {
// If descriptor is directly provided, use it; otherwise load from config
const descriptor =
options.descriptor ||
(await loadTextlintrc({
configFilePath: options.configFilePath,
node_modulesDir: options.node_modulesDir
}));
return {
descriptor,
ignoreFilePath: options.ignoreFilePath,
quiet: options.quiet,
cwd: options.cwd
};
};
// Helper functions for common MCP operations
const createStructuredErrorResponse = (error: string, type: TextlintMcpErrorType, isError = true) => {
const structuredContent = {
results: [],
error,
type,
timestamp: new Date().toISOString(),
isError
};
return {
content: [
{
type: "text" as const,
text: JSON.stringify(structuredContent, null, 2)
}
],
structuredContent,
isError
};
};
const createStructuredSuccessResponse = (data: Record<string, unknown> | object, isError = false) => {
const structuredContent = {
...data,
isError,
timestamp: new Date().toISOString()
};
return {
content: [
{
type: "text" as const,
text: JSON.stringify(structuredContent, null, 2)
}
],
structuredContent,
isError
};
};
const checkFilesExist = (filePaths: string[]) => {
return filePaths.filter((filePath) => !existsSync(filePath));
};
const validateInputAndReturnError = (value: string, fieldName: string, errorType: TextlintMcpErrorType) => {
if (!value.trim()) {
return createStructuredErrorResponse(`${fieldName} cannot be empty`, errorType);
}
return null;
};
export const setupServer = async (options: McpServerOptions = {}): Promise<McpServer> => {
const { readPackageUpSync } = await import("read-package-up");
const version = readPackageUpSync({ cwd: __dirname })?.packageJson.version ?? "unknown";
const server = new McpServer({
name: "textlint",
version
});
// ツール登録
server.registerTool(
"lintFile",
{
description: "Lint files using textlint",
inputSchema: {
filePaths: z
.array(z.string().min(1).describe("File path to lint"))
.nonempty()
.describe("Array of file paths to lint")
},
outputSchema: {
results: z.array(
z.object({
filePath: z.string(),
messages: z.array(TextlintMessageSchema),
output: z.string().optional().describe("Fixed content if available")
})
),
isError: z.boolean(),
timestamp: z.string().optional(),
error: z.string().optional(),
type: z.string().optional()
}
},
async ({ filePaths }) => {
try {
// Check if files exist before processing
const nonExistentFiles = checkFilesExist(filePaths);
if (nonExistentFiles.length > 0) {
return createStructuredErrorResponse(
`File(s) not found: ${nonExistentFiles.join(", ")}`,
"lintFile_error"
);
}
const linterOptions = await makeLinterOptions(options);
const linter = createLinter(linterOptions);
const results = await linter.lintFiles(filePaths);
// Return structured content as per MCP 2025-06-18 specification
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return createStructuredSuccessResponse({ results });
} catch (error) {
// Handle errors with isError flag for MCP compliance
return createStructuredErrorResponse(
error instanceof Error ? error.message : "Unknown error occurred",
"lintFile_error"
);
}
}
);
server.registerTool(
"lintText",
{
description: "Lint text using textlint",
inputSchema: {
text: z.string().nonempty().describe("Text content to lint"),
stdinFilename: z.string().nonempty().describe("Filename for context (e.g., 'stdin.md')")
},
outputSchema: {
filePath: z.string(),
messages: z.array(TextlintMessageSchema),
output: z.string().optional().describe("Fixed content if available"),
isError: z.boolean(),
timestamp: z.string().optional(),
error: z.string().optional(),
type: z.string().optional()
}
},
async ({ text, stdinFilename }) => {
try {
// Validate input parameters
const validationError = validateInputAndReturnError(stdinFilename, "stdinFilename", "lintText_error");
if (validationError) {
return validationError;
}
const linterOptions = await makeLinterOptions(options);
const linter = createLinter(linterOptions);
const result = await linter.lintText(text, stdinFilename);
// Return structured content as per MCP 2025-06-18 specification
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return createStructuredSuccessResponse(result);
} catch (error) {
return createStructuredErrorResponse(
error instanceof Error ? error.message : "Unknown error occurred",
"lintText_error"
);
}
}
);
server.registerTool(
"getLintFixedFileContent",
{
description: "Get lint-fixed content of files using textlint",
inputSchema: {
filePaths: z
.array(z.string().min(1).describe("File path to fix"))
.nonempty()
.describe("Array of file paths to get fixed content for")
},
outputSchema: {
results: z.array(
z.object({
filePath: z.string(),
messages: z.array(TextlintMessageSchema),
output: z.string().optional().describe("Fixed content")
})
),
isError: z.boolean(),
timestamp: z.string().optional(),
error: z.string().optional(),
type: z.string().optional()
}
},
async ({ filePaths }) => {
try {
// Check if files exist before processing
const nonExistentFiles = checkFilesExist(filePaths);
if (nonExistentFiles.length > 0) {
return createStructuredErrorResponse(
`File(s) not found: ${nonExistentFiles.join(", ")}`,
"fixFiles_error"
);
}
const linterOptions = await makeLinterOptions(options);
const linter = createLinter(linterOptions);
const results = await linter.fixFiles(filePaths);
// Return structured content as per MCP 2025-06-18 specification
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return createStructuredSuccessResponse({ results });
} catch (error) {
// Handle errors with isError flag for MCP compliance
return createStructuredErrorResponse(
error instanceof Error ? error.message : "Unknown error occurred",
"fixFiles_error"
);
}
}
);
server.registerTool(
"getLintFixedTextContent",
{
description: "Get lint-fixed content of text using textlint",
inputSchema: {
text: z.string().nonempty().describe("Text content to fix"),
stdinFilename: z.string().nonempty().describe("Filename for context (e.g., 'stdin.md')")
},
outputSchema: {
filePath: z.string(),
messages: z.array(TextlintMessageSchema),
output: z.string().optional().describe("Fixed content"),
isError: z.boolean(),
timestamp: z.string().optional(),
error: z.string().optional(),
type: z.string().optional()
}
},
async ({ text, stdinFilename }) => {
try {
// Validate input parameters
const validationError = validateInputAndReturnError(stdinFilename, "stdinFilename", "fixText_error");
if (validationError) return validationError;
const linterOptions = await makeLinterOptions(options);
const linter = createLinter(linterOptions);
const result = await linter.fixText(text, stdinFilename);
// Return structured content as per MCP 2025-06-18 specification
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return createStructuredSuccessResponse(result);
} catch (error) {
// Handle errors with isError flag for MCP compliance
return createStructuredErrorResponse(
error instanceof Error ? error.message : "Unknown error occurred",
"fixText_error"
);
}
}
);
return server;
};
export const connectStdioMcpServer = async (options: McpServerOptions = {}) => {
const server = await setupServer(options);
const transport = new StdioServerTransport();
await server.connect(transport);
return server;
};