textlint
Version:
The pluggable linting tool for natural language.
255 lines • 11.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.connectStdioMcpServer = exports.setupServer = void 0;
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const index_js_1 = require("../index.js");
const node_fs_1 = require("node:fs");
const makeLinterOptions = async () => {
const descriptor = await (0, index_js_1.loadTextlintrc)();
return {
descriptor
};
};
// Helper functions for common MCP operations
const createStructuredErrorResponse = (error, type, isError = true) => {
const structuredContent = {
results: [],
error,
type,
timestamp: new Date().toISOString(),
isError
};
return {
content: [
{
type: "text",
text: JSON.stringify(structuredContent, null, 2)
}
],
structuredContent,
isError
};
};
const createStructuredSuccessResponse = (data, isError = false) => {
const structuredContent = {
...data,
isError,
timestamp: new Date().toISOString()
};
return {
content: [
{
type: "text",
text: JSON.stringify(structuredContent, null, 2)
}
],
structuredContent,
isError
};
};
const checkFilesExist = (filePaths) => {
return filePaths.filter((filePath) => !(0, node_fs_1.existsSync)(filePath));
};
const validateInputAndReturnError = (value, fieldName, errorType) => {
if (!value.trim()) {
return createStructuredErrorResponse(`${fieldName} cannot be empty`, errorType);
}
return null;
};
const TextlintMessageSchema = zod_1.z
.object({
// Core properties
ruleId: zod_1.z.string().optional(),
message: zod_1.z.string(),
line: zod_1.z.number().describe("Line number (1-based)"),
column: zod_1.z.number().describe("Column number (1-based)"),
severity: zod_1.z.number().describe("Severity level: 1=warning, 2=error, 3=info"),
fix: zod_1.z
.object({
range: zod_1.z.array(zod_1.z.number()).describe("Text range [start, end] (0-based)"),
text: zod_1.z.string().describe("Replacement text")
})
.optional()
.describe("Fix suggestion if available"),
type: zod_1.z.string().optional().describe("Message type"),
data: zod_1.z.unknown().optional().describe("Optional data associated with the message"),
index: zod_1.z.number().optional().describe("Start index where the issue is located (0-based, deprecated)"),
range: zod_1.z.array(zod_1.z.number()).length(2).optional().describe("Text range [start, end] (0-based)"),
loc: zod_1.z
.object({
start: zod_1.z.object({
line: zod_1.z.number().describe("Start line number (1-based)"),
column: zod_1.z.number().describe("Start column number (1-based)")
}),
end: zod_1.z.object({
line: zod_1.z.number().describe("End line number (1-based)"),
column: zod_1.z.number().describe("End column number (1-based)")
})
})
.optional()
.describe("Location info where the issue is located")
})
.passthrough();
const setupServer = async () => {
var _a, _b;
const { readPackageUpSync } = await import("read-package-up");
const version = (_b = (_a = readPackageUpSync({ cwd: __dirname })) === null || _a === void 0 ? void 0 : _a.packageJson.version) !== null && _b !== void 0 ? _b : "unknown";
const server = new mcp_js_1.McpServer({
name: "textlint",
version
});
server.registerTool("lintFile", {
description: "Lint files using textlint",
inputSchema: {
filePaths: zod_1.z
.array(zod_1.z.string().min(1).describe("File path to lint"))
.nonempty()
.describe("Array of file paths to lint")
},
outputSchema: {
results: zod_1.z.array(zod_1.z.object({
filePath: zod_1.z.string(),
messages: zod_1.z.array(TextlintMessageSchema),
output: zod_1.z.string().optional().describe("Fixed content if available")
})),
isError: zod_1.z.boolean(),
timestamp: zod_1.z.string().optional(),
error: zod_1.z.string().optional(),
type: zod_1.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 = (0, index_js_1.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: zod_1.z.string().nonempty().describe("Text content to lint"),
stdinFilename: zod_1.z.string().nonempty().describe("Filename for context (e.g., 'stdin.md')")
},
outputSchema: {
filePath: zod_1.z.string(),
messages: zod_1.z.array(TextlintMessageSchema),
output: zod_1.z.string().optional().describe("Fixed content if available"),
isError: zod_1.z.boolean(),
timestamp: zod_1.z.string().optional(),
error: zod_1.z.string().optional(),
type: zod_1.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 = (0, index_js_1.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: zod_1.z
.array(zod_1.z.string().min(1).describe("File path to fix"))
.nonempty()
.describe("Array of file paths to get fixed content for")
},
outputSchema: {
results: zod_1.z.array(zod_1.z.object({
filePath: zod_1.z.string(),
messages: zod_1.z.array(TextlintMessageSchema),
output: zod_1.z.string().optional().describe("Fixed content")
})),
isError: zod_1.z.boolean(),
timestamp: zod_1.z.string().optional(),
error: zod_1.z.string().optional(),
type: zod_1.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 = (0, index_js_1.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: zod_1.z.string().nonempty().describe("Text content to fix"),
stdinFilename: zod_1.z.string().nonempty().describe("Filename for context (e.g., 'stdin.md')")
},
outputSchema: {
filePath: zod_1.z.string(),
messages: zod_1.z.array(TextlintMessageSchema),
output: zod_1.z.string().optional().describe("Fixed content"),
isError: zod_1.z.boolean(),
timestamp: zod_1.z.string().optional(),
error: zod_1.z.string().optional(),
type: zod_1.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 = (0, index_js_1.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;
};
exports.setupServer = setupServer;
const connectStdioMcpServer = async () => {
const server = await (0, exports.setupServer)();
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
return server;
};
exports.connectStdioMcpServer = connectStdioMcpServer;
//# sourceMappingURL=server.js.map