# Directory Structure ``` ├── .gitignore ├── eslint.config.mjs ├── LICENSE ├── nodemon.json ├── package.json ├── README.md ├── src │ ├── common │ │ ├── constants.ts │ │ ├── tools.ts │ │ ├── types.ts │ │ └── utils.ts │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | build 3 | 4 | .env 5 | .env.local 6 | .env.development 7 | .env.production 8 | .env.test 9 | .env.test.local 10 | .env.production.local 11 | 12 | 13 | yarn.lock 14 | package-lock.json 15 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Custom Context MCP Server 2 | 3 | This Model Context Protocol (MCP) server provides tools for structuring and extracting data from text according to JSON templates. 4 | 5 | ## Features 6 | 7 | ### Text-to-JSON Transformation 8 | 9 | - Group and structure text based on JSON templates with placeholders 10 | - Extract information from AI-generated text into structured JSON formats 11 | - Support for any arbitrary JSON structure with nested placeholders 12 | - Intelligent extraction of key-value pairs from text 13 | - Process AI outputs into structured data for downstream applications 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | ### Running the server 24 | 25 | ```bash 26 | npm start 27 | ``` 28 | 29 | For development with hot reloading: 30 | 31 | ```bash 32 | npm run dev:watch 33 | ``` 34 | 35 | ## Usage 36 | 37 | This MCP server provides two main tools: 38 | 39 | ### 1. Group Text by JSON (`group-text-by-json`) 40 | 41 | This tool takes a JSON template with placeholders and generates a prompt for an AI to group text according to the template's structure. 42 | 43 | ```json 44 | { 45 | "template": "{ \"type\": \"<type>\", \"text\": \"<text>\" }" 46 | } 47 | ``` 48 | 49 | The tool analyzes the template, extracts placeholder keys, and returns a prompt that guides the AI to extract information in a key-value format. 50 | 51 | ### 2. Text to JSON (`text-to-json`) 52 | 53 | This tool takes the grouped text output from the previous step and converts it into a structured JSON object based on the original template. 54 | 55 | ```json 56 | { 57 | "template": "{ \"type\": \"<type>\", \"text\": \"<text>\" }", 58 | "text": "type: pen\ntext: This is a blue pen" 59 | } 60 | ``` 61 | 62 | It extracts key-value pairs from the text and structures them according to the template. 63 | 64 | ## Example Workflow 65 | 66 | 1. **Define a JSON template with placeholders:** 67 | 68 | ```json 69 | { 70 | "item": { 71 | "name": "<name>", 72 | "price": "<price>", 73 | "description": "<description>" 74 | } 75 | } 76 | ``` 77 | 78 | 2. **Use `group-text-by-json` to create a prompt for AI:** 79 | 80 | - The tool identifies placeholder keys: name, price, description 81 | - Generates a prompt instructing the AI to group information by these keys 82 | 83 | 3. **Send the prompt to an AI model and receive grouped text:** 84 | 85 | ``` 86 | name: Blue Pen 87 | price: $2.99 88 | description: A smooth-writing ballpoint pen with blue ink 89 | ``` 90 | 91 | 4. **Use `text-to-json` to convert the grouped text to JSON:** 92 | - Result: 93 | ```json 94 | { 95 | "item": { 96 | "name": "Blue Pen", 97 | "price": "$2.99", 98 | "description": "A smooth-writing ballpoint pen with blue ink" 99 | } 100 | } 101 | ``` 102 | 103 | ## Template Format 104 | 105 | Templates can include placeholders anywhere within a valid JSON structure: 106 | 107 | - Use angle brackets to define placeholders: `<name>`, `<type>`, `<price>`, etc. 108 | - The template must be a valid JSON string 109 | - Placeholders can be at any level of nesting 110 | - Supports complex nested structures 111 | 112 | Example template with nested placeholders: 113 | 114 | ```json 115 | { 116 | "product": { 117 | "details": { 118 | "name": "<name>", 119 | "category": "<category>" 120 | }, 121 | "pricing": { 122 | "amount": "<price>", 123 | "currency": "USD" 124 | } 125 | }, 126 | "metadata": { 127 | "timestamp": "2023-09-01T12:00:00Z" 128 | } 129 | } 130 | ``` 131 | 132 | ## Implementation Details 133 | 134 | The server works by: 135 | 136 | 1. Analyzing JSON templates to extract placeholder keys 137 | 2. Generating prompts that guide AI models to extract information by these keys 138 | 3. Parsing AI-generated text to extract key-value pairs 139 | 4. Reconstructing JSON objects based on the original template structure 140 | 141 | ## Development 142 | 143 | ### Prerequisites 144 | 145 | - Node.js v18 or higher 146 | - npm or yarn 147 | 148 | ### Build and Run 149 | 150 | ```bash 151 | # Install dependencies 152 | npm install 153 | 154 | # Build the project 155 | npm run build 156 | 157 | # Run the server 158 | npm start 159 | 160 | # Development with hot reloading 161 | npm run dev:watch 162 | ``` 163 | 164 | ### Custom Hot Reloading 165 | 166 | This project includes a custom hot reloading setup that combines: 167 | 168 | - **nodemon**: Watches for file changes in the src directory and rebuilds TypeScript files 169 | - **browser-sync**: Automatically refreshes the browser when build files change 170 | - **Concurrent execution**: Runs both services simultaneously with output synchronization 171 | 172 | The setup is configured in: 173 | 174 | - `nodemon.json`: Controls TypeScript watching and rebuilding 175 | - `package.json`: Uses concurrently to run nodemon and browser-sync together 176 | 177 | To use the custom hot reloading feature: 178 | 179 | ```bash 180 | npm run dev:watch 181 | ``` 182 | 183 | This creates a development environment where: 184 | 185 | 1. TypeScript files are automatically rebuilt when changed 186 | 2. The MCP server restarts with the updated code 187 | 3. Connected browsers refresh to show the latest changes 188 | 189 | ### Using with MCP Inspector 190 | 191 | You can use the MCP Inspector for debugging: 192 | 193 | ```bash 194 | npm run dev 195 | ``` 196 | 197 | This runs the server with the MCP Inspector for visual debugging of requests and responses. 198 | ``` -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- ``` 1 | import { defineConfig } from "eslint/config"; 2 | import globals from "globals"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | 6 | export default defineConfig([ 7 | { files: ["**/*.{js,mjs,cjs,ts}"] }, 8 | { files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.browser } }, 9 | tseslint.configs.recommended, 10 | ]); ``` -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "watch": ["src/**/*.ts"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts", "build/"], 5 | "exec": "mcp-inspector node build/index.js", 6 | "events": { 7 | "restart": "tsc && chmod 755 build/index.js && echo 'Built TypeScript...'", 8 | "start": "tsc && chmod 755 build/index.js && echo 'Built TypeScript (initial)...'" 9 | } 10 | } 11 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | const VERSION = "0.0.1"; 4 | 5 | const MCP_SERVER_NAME = "custom-context-mcp"; 6 | const TOOL_NAMES = { 7 | groupTextByJson: "group-text-by-json", 8 | textToJson: "text-to-json", 9 | }; 10 | const MCP_CAPABILITIES: ServerCapabilities = { 11 | tools: {}, 12 | }; 13 | 14 | export { VERSION, MCP_SERVER_NAME, TOOL_NAMES, MCP_CAPABILITIES }; 15 | ``` -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import z from "zod"; 2 | 3 | const GroupTextByJsonSchema = z.object({ 4 | template: z.string().describe("JSON template with placeholders"), 5 | }); 6 | const TextToJsonSchema = z.object({ 7 | template: z.string().describe("JSON template with placeholders"), 8 | text: z.string().describe("Groupped text from groupTextByJson tool"), 9 | }); 10 | 11 | export type GroupTextByJsonSchemaType = z.infer<typeof GroupTextByJsonSchema>; 12 | export type TextToJsonSchemaType = z.infer<typeof TextToJsonSchema>; 13 | 14 | export { GroupTextByJsonSchema, TextToJsonSchema }; 15 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "custom-context-mcp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "custom-context-mcp": "build/index.js" 9 | }, 10 | "scripts": { 11 | "dev": "yarn build && mcp-inspector node build/index.js", 12 | "dev:watch": "concurrently \"nodemon\" \"browser-sync start --proxy localhost:6274 --files build/**/* --no-notify --port 3001 --reload-delay 1000\"", 13 | "build": "tsc && chmod 755 build/index.js", 14 | "check:types": "tsc --noEmit", 15 | "check:lint": "eslint . --ext .ts", 16 | "check": "npm run check:types && npm run check:lint", 17 | "start": "node build/index.js", 18 | "test:samples": "yarn build && node build/test-samples.js" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@modelcontextprotocol/sdk": "^1.8.0", 25 | "zod": "^3.24.2", 26 | "zod-to-json-schema": "^3.24.5" 27 | }, 28 | "devDependencies": { 29 | "@modelcontextprotocol/inspector": "^0.8.1", 30 | "@types/node": "^22.14.0", 31 | "browser-sync": "^3.0.4", 32 | "concurrently": "^9.1.2", 33 | "eslint": "^9.24.0", 34 | "globals": "^16.0.0", 35 | "nodemon": "^3.1.9", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.8.3", 38 | "typescript-eslint": "^8.29.0" 39 | }, 40 | "files": [ 41 | "build" 42 | ] 43 | } 44 | ``` -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | const logger = { 2 | info: (...args: any[]) => { 3 | const msg = `[INFO] ${args.join(" ")}`; 4 | process.stderr.write(`${msg}\n`); 5 | }, 6 | debug: (...args: any[]) => { 7 | const msg = `\x1b[36m[DEBUG]\x1b[0m ${args.join(" ")}`; 8 | process.stderr.write(`${msg}\n`); 9 | }, 10 | warn: (...args: any[]) => { 11 | const msg = `\x1b[33m[WARN]\x1b[0m ${args.join(" ")}`; 12 | process.stderr.write(`${msg}\n`); 13 | }, 14 | error: (...args: any[]) => { 15 | const msg = `\x1b[31m[ERROR]\x1b[0m ${args.join(" ")}`; 16 | process.stderr.write(`${msg}\n`); 17 | }, 18 | }; 19 | 20 | function deepObjectKeys(obj: any, onlyPlaceholders: boolean = false): string[] { 21 | const keys: string[] = []; 22 | 23 | function traverseObject(obj: Record<string, any>, prefix: string = "") { 24 | if (typeof obj !== "object" || !obj) return; 25 | 26 | for (const objKey of Object.keys(obj)) { 27 | const currObj = obj[objKey] as Record<string, any> | string; 28 | const keyName = prefix ? `${prefix}.${objKey}` : objKey; 29 | 30 | if (onlyPlaceholders) { 31 | const bracketsRegex = /<[^>]+>/gi; 32 | if (typeof currObj === "string" && bracketsRegex.test(currObj)) { 33 | keys.push(keyName); 34 | } 35 | } else { 36 | keys.push(keyName); 37 | } 38 | 39 | if (typeof currObj === "object" && !Array.isArray(currObj) && !!currObj) { 40 | traverseObject(currObj, objKey); 41 | } 42 | } 43 | } 44 | 45 | traverseObject(obj); 46 | return keys; 47 | } 48 | 49 | function extractKeyValuesFromText(text: string, keys: string[]) { 50 | const regex = new RegExp(`(${keys.join("|")}): (.*?)(\n|$)`, "gi"); 51 | const matches = text.match(regex); 52 | 53 | if (!matches) { 54 | return {}; 55 | } 56 | 57 | const result: Record<string, string> = {}; 58 | 59 | matches.forEach((match) => { 60 | const [key, value] = match.split(":"); 61 | const extractedKey = key.trim(); 62 | const extractedValues = value.trim(); 63 | 64 | if (extractedKey) { 65 | result[extractedKey] = extractedValues ?? ""; 66 | } 67 | }); 68 | 69 | return result; 70 | } 71 | 72 | export { logger, deepObjectKeys, extractKeyValuesFromText }; 73 | ``` -------------------------------------------------------------------------------- /src/common/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { deepObjectKeys, extractKeyValuesFromText, logger } from "./utils.js"; 2 | 3 | const groupTextByJsonTool = (template: string) => { 4 | if (!template) { 5 | throw new Error("Both template and text are required"); 6 | } 7 | 8 | try { 9 | logger.info("Template:", template); 10 | 11 | let objectKeys: string[] = []; 12 | 13 | try { 14 | const templateObj = JSON.parse(template); 15 | objectKeys = deepObjectKeys(templateObj, true); 16 | } catch (parseError) { 17 | logger.error("Failed to parse template:", parseError); 18 | throw new Error(`Invalid template format: ${parseError}`); 19 | } 20 | 21 | const resultPrompt = ` 22 | You are a helpful assistant that groups text based on JSON keys. 23 | Here are the keys in the template: ${objectKeys.join(", ")}. 24 | Please group the text based on the keys. and give me the result in raw text. 25 | Don't give it in JSON format or object format. It should be in the following format: 26 | 27 | Format: 28 | <key>: <corresponding text found in the text> 29 | 30 | Here's an example: 31 | 32 | sentence: The MacBook Pro costs $2,499. 33 | 34 | result: 35 | brand: MacBook 36 | price: $2,499 37 | description: The MacBook Pro is a powerful laptop with a Retina display. 38 | 39 | `; 40 | 41 | return { 42 | content: [ 43 | { 44 | type: "text", 45 | text: resultPrompt, 46 | }, 47 | ], 48 | }; 49 | } catch (error) { 50 | logger.error("Error processing template:", error); 51 | throw new Error(`Failed to process template: ${error}`); 52 | } 53 | }; 54 | 55 | const textToJsonTool = (template: string, text: string) => { 56 | if (!template || !text) { 57 | throw new Error("Both template and text are required"); 58 | } 59 | 60 | try { 61 | const templateObj = JSON.parse(template); 62 | const templateKeys = deepObjectKeys(templateObj, true); 63 | 64 | const jsonResult = extractKeyValuesFromText(text, templateKeys); 65 | 66 | const resultPrompt = ` 67 | Print this JSON result in JSON format. 68 | 69 | JSON result: 70 | ${JSON.stringify(jsonResult)} 71 | 72 | `; 73 | 74 | return { 75 | content: [ 76 | { 77 | type: "text", 78 | text: resultPrompt, 79 | }, 80 | ], 81 | }; 82 | } catch (error) { 83 | logger.error("Error processing template:", error); 84 | throw new Error(`Failed to process template: ${error}`); 85 | } 86 | }; 87 | 88 | export { groupTextByJsonTool, textToJsonTool }; 89 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { 4 | CallToolRequestSchema, 5 | ListToolsRequestSchema, 6 | } from "@modelcontextprotocol/sdk/types.js"; 7 | 8 | import { zodToJsonSchema } from "zod-to-json-schema"; 9 | 10 | import { logger } from "./common/utils.js"; 11 | import { 12 | GroupTextByJsonSchema, 13 | GroupTextByJsonSchemaType, 14 | TextToJsonSchema, 15 | TextToJsonSchemaType, 16 | } from "./common/types.js"; 17 | import { groupTextByJsonTool, textToJsonTool } from "./common/tools.js"; 18 | import { 19 | MCP_CAPABILITIES, 20 | MCP_SERVER_NAME, 21 | TOOL_NAMES, 22 | VERSION, 23 | } from "./common/constants.js"; 24 | 25 | const server = new Server( 26 | { 27 | name: MCP_SERVER_NAME, 28 | version: VERSION, 29 | }, 30 | { 31 | capabilities: MCP_CAPABILITIES, 32 | } 33 | ); 34 | 35 | server.setRequestHandler(ListToolsRequestSchema, async () => { 36 | logger.info("[MCP] Received tools/list request"); 37 | const response = { 38 | tools: [ 39 | { 40 | name: TOOL_NAMES.groupTextByJson, 41 | description: 42 | "Gives a prompt text for AI to group text based on JSON placeholders. This tool accepts a JSON template with placeholders.", 43 | inputSchema: zodToJsonSchema(GroupTextByJsonSchema), 44 | }, 45 | { 46 | name: TOOL_NAMES.textToJson, 47 | description: `Converts groupped text from ${TOOL_NAMES.groupTextByJson} tool to JSON. This tool accepts a JSON template with placeholders and groupped text from ${TOOL_NAMES.groupTextByJson} tool.`, 48 | inputSchema: zodToJsonSchema(TextToJsonSchema), 49 | }, 50 | ], 51 | }; 52 | 53 | return response; 54 | }); 55 | 56 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 57 | const { name, arguments: args } = request.params; 58 | logger.info(`[MCP] Received tools/call request for tool: ${name}`); 59 | logger.debug(`[MCP] Tool arguments: ${JSON.stringify(args, null, 2)}`); 60 | 61 | if (!args) { 62 | throw new Error("Arguments are required"); 63 | } 64 | 65 | switch (name) { 66 | case TOOL_NAMES.groupTextByJson: 67 | const groupTextByJsonArgs = args as GroupTextByJsonSchemaType; 68 | return groupTextByJsonTool(groupTextByJsonArgs.template); 69 | case TOOL_NAMES.textToJson: 70 | const textToJsonArgs = args as TextToJsonSchemaType; 71 | return textToJsonTool(textToJsonArgs.template, textToJsonArgs.text); 72 | default: 73 | throw new Error(`Unknown tool: ${name}`); 74 | } 75 | }); 76 | 77 | async function runServer() { 78 | try { 79 | logger.info("Initializing MCP Server..."); 80 | const transport = new StdioServerTransport(); 81 | 82 | logger.info("Connecting to transport..."); 83 | await server.connect(transport); 84 | 85 | logger.info("Custom Context MCP Server running on stdio"); 86 | logger.info( 87 | "Server information:", 88 | JSON.stringify({ 89 | name: MCP_SERVER_NAME, 90 | tools: [TOOL_NAMES.groupTextByJson], 91 | }) 92 | ); 93 | 94 | logger.info("MCP Server is ready to accept requests"); 95 | 96 | process.on("SIGINT", () => { 97 | logger.info("Received SIGINT signal, shutting down..."); 98 | process.exit(0); 99 | }); 100 | 101 | process.on("SIGTERM", () => { 102 | logger.info("Received SIGTERM signal, shutting down..."); 103 | process.exit(0); 104 | }); 105 | 106 | process.on("uncaughtException", (error: Error) => { 107 | logger.error("Uncaught exception:", error); 108 | }); 109 | } catch (error) { 110 | logger.error("Fatal error during server initialization:", error); 111 | process.exit(1); 112 | } 113 | } 114 | 115 | runServer().catch((error) => { 116 | logger.error("Fatal error while running server:", error); 117 | if (error instanceof Error) { 118 | logger.error("Stack trace:", error.stack); 119 | } 120 | process.exit(1); 121 | }); 122 | ```