textlint
Version:
The pluggable linting tool for natural language.
309 lines (281 loc) • 11.8 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";
const makeLinterOptions = async (): Promise<CreateLinterOptions> => {
const descriptor = await loadTextlintrc();
return {
descriptor
};
};
// Helper functions for common MCP operations
const createStructuredErrorResponse = (error: string, type: string, 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: string) => {
if (!value.trim()) {
return createStructuredErrorResponse(`${fieldName} cannot be empty`, errorType);
}
return null;
};
const TextlintMessageSchema = z
.object({
// Core properties
ruleId: z.string().optional(),
message: z.string(),
line: z.number().describe("Line number (1-based)"),
column: z.number().describe("Column number (1-based)"),
severity: z.number().describe("Severity level: 1=warning, 2=error, 3=info"),
fix: z
.object({
range: z.array(z.number()).describe("Text range [start, end] (0-based)"),
text: z.string().describe("Replacement text")
})
.optional()
.describe("Fix suggestion if available"),
type: z.string().optional().describe("Message type"),
data: z.unknown().optional().describe("Optional data associated with the message"),
index: z.number().optional().describe("Start index where the issue is located (0-based, deprecated)"),
range: z.array(z.number()).length(2).optional().describe("Text range [start, end] (0-based)"),
loc: z
.object({
start: z.object({
line: z.number().describe("Start line number (1-based)"),
column: z.number().describe("Start column number (1-based)")
}),
end: z.object({
line: z.number().describe("End line number (1-based)"),
column: z.number().describe("End column number (1-based)")
})
})
.optional()
.describe("Location info where the issue is located")
})
.passthrough();
export const setupServer = async (): 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();
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();
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();
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();
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 () => {
const server = await setupServer();
const transport = new StdioServerTransport();
await server.connect(transport);
return server;
};