# 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 |
```