markmv
Version:
TypeScript CLI for markdown file operations with intelligent link refactoring
187 lines • 8.28 kB
JavaScript
/**
* MCP Server implementation for markmv
*
* Provides Model Context Protocol server that exposes markmv functionality as tools for AI agents.
* Uses auto-generated tool definitions from JSON Schema-first approach. Allows seamless integration
* with Claude and other MCP clients.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { createMarkMv } from './index.js';
import { autoGeneratedMcpTools, getMcpToolNames } from './generated/mcp-tools.js';
import { validateInput } from './generated/ajv-validators.js';
const markmv = createMarkMv();
/** Convert snake_case to camelCase for method name mapping */
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/** Type guard to check if an object is a valid OperationResult */
function isOperationResult(obj) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
return false;
}
// Since we've checked it's an object above, this is safe
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const record = obj;
return (typeof record.success === 'boolean' &&
Array.isArray(record.modifiedFiles) &&
Array.isArray(record.createdFiles) &&
Array.isArray(record.deletedFiles) &&
Array.isArray(record.errors) &&
Array.isArray(record.warnings) &&
Array.isArray(record.changes));
}
/** Create and configure the MCP server for markmv */
export function createMcpServer() {
const server = new Server({
name: 'markmv-mcp',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// List available tools (auto-generated)
server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
return {
tools: autoGeneratedMcpTools,
};
});
// Handle tool calls (auto-generated with validation)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// Validate tool name
const availableTools = getMcpToolNames();
if (!availableTools.includes(name)) {
throw new Error(`Unknown tool: ${name}. Available tools: ${availableTools.join(', ')}`);
}
// Convert snake_case tool name back to camelCase method name
const methodName = snakeToCamel(name);
// Validate input using auto-generated validators
const validation = validateInput(methodName, args);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors?.join(', ')}`);
}
// Route to appropriate method with proper type checking
let result;
if (methodName === 'moveFile') {
if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argsObj = args;
const sourcePath = argsObj.sourcePath;
const destinationPath = argsObj.destinationPath;
const options = argsObj.options || {};
if (typeof sourcePath === 'string' &&
typeof destinationPath === 'string' &&
typeof options === 'object' &&
options !== null &&
!Array.isArray(options)) {
result = await markmv.moveFile(sourcePath, destinationPath,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options);
}
else {
throw new Error('Invalid parameters for moveFile: sourcePath and destinationPath must be strings');
}
}
else {
throw new Error('Invalid arguments object for moveFile');
}
}
else if (methodName === 'moveFiles') {
if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argsObj = args;
const moves = argsObj.moves;
const options = argsObj.options || {};
if (Array.isArray(moves) &&
typeof options === 'object' &&
options !== null &&
!Array.isArray(options)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
result = await markmv.moveFiles(moves, options);
}
else {
throw new Error('Invalid parameters for moveFiles: moves must be an array');
}
}
else {
throw new Error('Invalid arguments object for moveFiles');
}
}
else if (methodName === 'validateOperation') {
if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argsObj = args;
const operationResult = argsObj.result;
if (isOperationResult(operationResult)) {
result = await markmv.validateOperation(operationResult);
}
else {
throw new Error('Invalid OperationResult structure: missing required properties');
}
}
else {
throw new Error('Invalid arguments object for validateOperation');
}
}
else if (methodName === 'testAutoExposure') {
if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argsObj = args;
const input = argsObj.input;
if (typeof input === 'string') {
const { testAutoExposure } = await import('./index.js');
result = await testAutoExposure(input);
}
else {
throw new Error('Invalid parameters for testAutoExposure: input must be a string');
}
}
else {
throw new Error('Invalid arguments object for testAutoExposure');
}
}
else {
throw new Error(`Method ${methodName} not implemented`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
return server;
}
/** Start the MCP server */
export async function startMcpServer() {
const server = createMcpServer();
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('markmv MCP server started');
}
// For direct execution
if (process.argv[1] && process.argv[1].endsWith('mcp-server.js')) {
startMcpServer().catch((error) => {
console.error('Failed to start MCP server:', error);
process.exit(1);
});
}
//# sourceMappingURL=mcp-server.js.map