Skip to content

Commit 246753e

Browse files
committed
Initial LAI MCP server #5
1 parent 12a2bcb commit 246753e

File tree

12 files changed

+1627
-273
lines changed

12 files changed

+1627
-273
lines changed

package-lock.json

Lines changed: 1356 additions & 271 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"license": "MIT",
1515
"workspaces": [
1616
"packages/langium-ai-tools",
17+
"packages/langium-ai-mcp",
1718
"packages/examples/*"
1819
],
1920
"volta": {

packages/examples/example-dsl-evaluator/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"build": "tsc",
99
"start": "node ./dist/index.js",
1010
"demo": "npm run build && npm run start -- run-langdev && open ./radar-chart.html",
11-
"clean": "rimraf ./dist"
11+
"clean": "rimraf ./dist",
12+
"test": "echo \"No tests yet...\""
1213
},
1314
"type": "module",
1415
"author": {

packages/langium-ai-mcp/README.MD

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
How to try-out:
2+
3+
- `cd packages/langium-ai-mcp`
4+
- Start MCP server with IO transport `npm run start`
5+
- Run example client code `npm run cstart` - you should see the tool result containing the errors.
6+
7+
Example usage in Cursor:
8+
9+
- Open Cursor MCP settings
10+
- Add new server using following setup (user or workspace specific `.cursor/mcp.json` ):
11+
12+
```json
13+
"mcpServers": {
14+
"Langium MCP": {
15+
"command": "node",
16+
"args": [
17+
"~/git/langium-ai/packages/mcp-server/dist/mcp-server.js"
18+
]
19+
}
20+
}
21+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "langium-ai-mcp",
3+
"version": "0.0.2",
4+
"displayName": "Langium AI - MCP",
5+
"publisher": "TypeFox",
6+
"description": "MCP server for Langium AI",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/eclipse-langium/langium-ai.git",
10+
"directory": "packages/langium-ai-mcp"
11+
},
12+
"bugs": "https://github.com/eclipse-langium/langium-ai/issues",
13+
"type": "module",
14+
"main": "dist/mcp-server.js",
15+
"private": false,
16+
"files": [
17+
"dist"
18+
],
19+
"scripts": {
20+
"clean": "rm -rf ./dist",
21+
"build": "npm run clean && tsc",
22+
"watch": "tsc -w",
23+
"start": "node .",
24+
"cstart": "node ./dist/mcp-client.js",
25+
"prepare": "npm run build",
26+
"test": "vitest run"
27+
},
28+
"author": {
29+
"name": "TypeFox",
30+
"url": "https://www.typefox.io"
31+
},
32+
"keywords": [
33+
"langium",
34+
"ai",
35+
"mcp",
36+
"server",
37+
"llm"
38+
],
39+
"license": "MIT",
40+
"dependencies": {
41+
"@modelcontextprotocol/sdk": "^1.17.4",
42+
"langium-ai-tools": "0.0.2"
43+
},
44+
"volta": {
45+
"node": "20.10.0",
46+
"npm": "10.2.3"
47+
},
48+
"devDependencies": {
49+
"typescript": "^5.4.5",
50+
"vitest": "^3.0.9"
51+
}
52+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3+
import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
4+
5+
const debug = false
6+
7+
const transport = new StdioClientTransport({
8+
command: "node",
9+
args: [ "./dist/mcp-server.js"]
10+
});
11+
12+
const client = new Client(
13+
{
14+
name: "example-client",
15+
version: "1.0.0"
16+
}
17+
);
18+
19+
await client.connect(transport);
20+
21+
const tools = await client.listTools();
22+
console.log("Available tools:", "\n", ...tools.tools.map(t => getDisplayName(t) + "\n"));
23+
24+
const theTool = tools.tools[0];
25+
if (!theTool) {
26+
throw new Error("No tool available");
27+
}
28+
29+
const result = await client.callTool({
30+
name: theTool.name,
31+
arguments: {
32+
code: 'syntax error'
33+
}
34+
});
35+
36+
console.log("Tool result:", result.content);
37+
38+
// exit the process
39+
process.exit(0);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3+
import { LangiumEvaluator, type LangiumEvaluatorResultData } from 'langium-ai-tools';
4+
import { createLangiumGrammarServices } from 'langium/grammar';
5+
6+
import { NodeFileSystem } from 'langium/node';
7+
import { z } from 'zod';
8+
9+
const server = new McpServer({
10+
name: 'langium-mpc-server',
11+
version: '1.0.0'
12+
});
13+
14+
server.registerTool('langium-syntax-checker',
15+
{
16+
title: 'Langium Evaluator Tool',
17+
description: 'Checks Langium code for errors',
18+
inputSchema: { code: z.string() }
19+
},
20+
async ({ code }) => {
21+
const validationResult = await validateLangiumCode(code);
22+
return {
23+
content: [
24+
{
25+
type: 'text',
26+
text: validationResult ?? 'The provided Langium code has no issues.'
27+
}
28+
]
29+
}
30+
}
31+
);
32+
33+
export const langiumEvaluator = new LangiumEvaluator(createLangiumGrammarServices(NodeFileSystem).grammar);
34+
35+
export async function validateLangiumCode(code: string): Promise<string | undefined> {
36+
const evalResult = await langiumEvaluator.evaluate(code);
37+
if (evalResult.data) {
38+
const langiumData = evalResult.data as LangiumEvaluatorResultData;
39+
if (langiumData.diagnostics.length > 0) {
40+
return langiumData.diagnostics.map(d =>
41+
`${asText(d.severity)}: ${d.message} at line ${d.range.start.line + 1}, column ${d.range.start.character + 1}`
42+
).join('\n');
43+
}
44+
}
45+
return undefined;
46+
}
47+
48+
function asText(severity: number | undefined): string {
49+
50+
switch (severity) {
51+
case 1: return 'Error';
52+
case 2: return 'Warning';
53+
case 3: return 'Information';
54+
case 4: return 'Hint';
55+
default: return 'Unknown';
56+
}
57+
}
58+
59+
const transport = new StdioServerTransport();
60+
await server.connect(transport);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { validateLangiumCode } from '../src/mcp-server';
4+
5+
describe('validateLangiumCode', () => {
6+
7+
8+
it('should return undefined for valid grammar code', async () => {
9+
const validCode = `
10+
grammar HelloWorld
11+
12+
entry Model: persons+=Person*;
13+
Person: 'person' name=ID;
14+
hidden terminal WS: /\\s+/;
15+
terminal ID: /[_a-zA-Z][\\w_]*/;
16+
`;
17+
18+
const result = await validateLangiumCode(validCode);
19+
expect(result).toBeUndefined();
20+
});
21+
22+
it('should return diagnostics for invalid grammar code', async () => {
23+
const invalidCode = `
24+
grammar HelloWorld
25+
entry Model: persons+=Person*;
26+
`;
27+
28+
const result = await validateLangiumCode(invalidCode);
29+
expect(result).toBeDefined();
30+
expect(result).toContain("Error: Could not resolve reference to AbstractRule named 'Person'. at line 3, column 35");
31+
});
32+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"compilerOptions": {
3+
"rootDir": "./src",
4+
"outDir": "./dist",
5+
"module": "nodenext",
6+
"target": "esnext",
7+
"types": [
8+
"node"
9+
],
10+
"sourceMap": true,
11+
"declaration": true,
12+
"declarationMap": true,
13+
"noUncheckedIndexedAccess": true,
14+
"exactOptionalPropertyTypes": true,
15+
"strict": true,
16+
"jsx": "react-jsx",
17+
"verbatimModuleSyntax": true,
18+
"isolatedModules": true,
19+
"noUncheckedSideEffectImports": true,
20+
"moduleDetection": "force",
21+
"skipLibCheck": true,
22+
"forceConsistentCasingInFileNames": true
23+
},
24+
"include": [
25+
"src"
26+
],
27+
"exclude": [
28+
"node_modules",
29+
"tests",
30+
"dist"
31+
]
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"types": ["vitest/globals", "node"]
5+
},
6+
"include": [
7+
"src/**/*",
8+
"tests/**/*"
9+
],
10+
"exclude": [
11+
"node_modules",
12+
"dist"
13+
]
14+
}

0 commit comments

Comments
 (0)