@likec4/language-server 1.28.1 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LikeC4LanguageServices.d.ts +6 -1
- package/dist/LikeC4LanguageServices.js +18 -1
- package/dist/bundled.mjs +2447 -2185
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/lsp/SemanticTokenProvider.js +5 -28
- package/dist/mcp/LikeC4MCPServerFactory.d.ts +16 -0
- package/dist/mcp/LikeC4MCPServerFactory.js +86 -0
- package/dist/mcp/LikeC4MCPTools.d.ts +96 -0
- package/dist/mcp/LikeC4MCPTools.js +288 -0
- package/dist/mcp/sseserver/SSELikeC4MCPServer.d.ts +13 -0
- package/dist/mcp/sseserver/SSELikeC4MCPServer.js +74 -0
- package/dist/mcp/sseserver/with-mcp-server.d.ts +7 -0
- package/dist/mcp/sseserver/with-mcp-server.js +33 -0
- package/dist/mcp/utils.d.ts +34 -0
- package/dist/mcp/utils.js +104 -0
- package/dist/model/parser/Base.js +3 -0
- package/dist/module.d.ts +7 -0
- package/dist/module.js +7 -0
- package/dist/workspace/ProjectsManager.js +1 -1
- package/package.json +11 -7
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { getLspConnectionSink, logger as lspLogger } from './logger';
|
|
|
4
4
|
export type { DocumentParser, LikeC4ModelBuilder, LikeC4ModelLocator, LikeC4ModelParser } from './model';
|
|
5
5
|
export type { LikeC4LanguageServices } from './LikeC4LanguageServices';
|
|
6
6
|
export { isLikeC4Builtin } from './likec4lib';
|
|
7
|
+
export { LikeC4MCPTools } from './mcp/LikeC4MCPTools';
|
|
7
8
|
export { createCustomLanguageServices, createLanguageServices, LikeC4Module } from './module';
|
|
8
9
|
export type { LikeC4Services, LikeC4SharedServices } from './module';
|
|
9
10
|
export type { LikeC4Views } from './views';
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,12 @@ import { startLanguageServer as startLanguim } from "langium/lsp";
|
|
|
3
3
|
import { createConnection, ProposedFeatures } from "vscode-languageserver/node";
|
|
4
4
|
import { LikeC4FileSystem } from "./LikeC4FileSystem.js";
|
|
5
5
|
import { getTelemetrySink, logger } from "./logger.js";
|
|
6
|
+
import { WithMCPServer } from "./mcp/sseserver/with-mcp-server.js";
|
|
6
7
|
import { createCustomLanguageServices } from "./module.js";
|
|
7
8
|
import { ConfigurableLayouter } from "./views/configurable-layouter.js";
|
|
8
9
|
export { getLspConnectionSink, logger as lspLogger } from "./logger.js";
|
|
9
10
|
export { isLikeC4Builtin } from "./likec4lib.js";
|
|
11
|
+
export { LikeC4MCPTools } from "./mcp/LikeC4MCPTools.js";
|
|
10
12
|
export { createCustomLanguageServices, createLanguageServices, LikeC4Module } from "./module.js";
|
|
11
13
|
export { LikeC4FileSystem };
|
|
12
14
|
export function startLanguageServer() {
|
|
@@ -26,7 +28,11 @@ export function startLanguageServer() {
|
|
|
26
28
|
]
|
|
27
29
|
});
|
|
28
30
|
logger.info("Starting LikeC4 language server");
|
|
29
|
-
const services = createCustomLanguageServices(
|
|
31
|
+
const services = createCustomLanguageServices(
|
|
32
|
+
{ connection, ...LikeC4FileSystem },
|
|
33
|
+
ConfigurableLayouter,
|
|
34
|
+
WithMCPServer
|
|
35
|
+
);
|
|
30
36
|
startLanguim(services.shared);
|
|
31
37
|
return services;
|
|
32
38
|
}
|
|
@@ -27,7 +27,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
27
27
|
});
|
|
28
28
|
return "prune";
|
|
29
29
|
}
|
|
30
|
-
if (ast.isRelation(node) || ast.isOutgoingRelationExpr(node) && "kind" in node) {
|
|
30
|
+
if ((ast.isRelation(node) || ast.isOutgoingRelationExpr(node) || ast.isDeploymentRelation(node)) && "kind" in node) {
|
|
31
31
|
acceptor({
|
|
32
32
|
node,
|
|
33
33
|
property: "kind",
|
|
@@ -43,7 +43,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
43
43
|
acceptor({
|
|
44
44
|
node,
|
|
45
45
|
property: "value",
|
|
46
|
-
type: SemanticTokenTypes.
|
|
46
|
+
type: SemanticTokenTypes.interface,
|
|
47
47
|
modifier: [
|
|
48
48
|
SemanticTokenModifiers.definition,
|
|
49
49
|
SemanticTokenModifiers.readonly
|
|
@@ -60,17 +60,6 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
60
60
|
SemanticTokenModifiers.readonly
|
|
61
61
|
]
|
|
62
62
|
});
|
|
63
|
-
if (ast.isFqnRefExpr(node) && node.selector) {
|
|
64
|
-
acceptor({
|
|
65
|
-
node,
|
|
66
|
-
property: "selector",
|
|
67
|
-
type: SemanticTokenTypes.variable,
|
|
68
|
-
modifier: [
|
|
69
|
-
SemanticTokenModifiers.definition,
|
|
70
|
-
SemanticTokenModifiers.readonly
|
|
71
|
-
]
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
63
|
return "prune";
|
|
75
64
|
}
|
|
76
65
|
if (ast.isWhereRelationKind(node) && isTruthy(node.value)) {
|
|
@@ -107,7 +96,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
107
96
|
type: SemanticTokenTypes.type,
|
|
108
97
|
modifier: [SemanticTokenModifiers.definition]
|
|
109
98
|
});
|
|
110
|
-
return
|
|
99
|
+
return;
|
|
111
100
|
}
|
|
112
101
|
if (ast.isGlobalStyleGroup(node) || ast.isGlobalStyle(node)) {
|
|
113
102
|
acceptor({
|
|
@@ -164,7 +153,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
164
153
|
type: SemanticTokenTypes.type,
|
|
165
154
|
modifier: [SemanticTokenModifiers.definition]
|
|
166
155
|
});
|
|
167
|
-
return
|
|
156
|
+
return;
|
|
168
157
|
}
|
|
169
158
|
if (ast.isFqnRef(node) || ast.isStrictFqnRef(node)) {
|
|
170
159
|
acceptor({
|
|
@@ -190,18 +179,6 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
190
179
|
});
|
|
191
180
|
return !node.parent ? "prune" : void 0;
|
|
192
181
|
}
|
|
193
|
-
if (ast.isImported(node)) {
|
|
194
|
-
acceptor({
|
|
195
|
-
node,
|
|
196
|
-
property: "imported",
|
|
197
|
-
type: SemanticTokenTypes.variable,
|
|
198
|
-
modifier: [
|
|
199
|
-
SemanticTokenModifiers.definition,
|
|
200
|
-
SemanticTokenModifiers.readonly
|
|
201
|
-
]
|
|
202
|
-
});
|
|
203
|
-
return "prune";
|
|
204
|
-
}
|
|
205
182
|
if (ast.isSpecificationColor(node)) {
|
|
206
183
|
acceptor({
|
|
207
184
|
node,
|
|
@@ -310,7 +287,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
310
287
|
acceptor({
|
|
311
288
|
node,
|
|
312
289
|
property: "name",
|
|
313
|
-
type: SemanticTokenTypes.
|
|
290
|
+
type: SemanticTokenTypes.variable,
|
|
314
291
|
modifier: [
|
|
315
292
|
SemanticTokenModifiers.declaration,
|
|
316
293
|
SemanticTokenModifiers.readonly
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { LikeC4Services } from '../module';
|
|
4
|
+
export interface LikeC4MCPServer {
|
|
5
|
+
start(port: number): Promise<void>;
|
|
6
|
+
stop(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare class NoopLikeC4MCPServer implements LikeC4MCPServer {
|
|
9
|
+
start(port: number): Promise<never>;
|
|
10
|
+
stop(): Promise<never>;
|
|
11
|
+
}
|
|
12
|
+
export declare class LikeC4MCPServerFactory {
|
|
13
|
+
private services;
|
|
14
|
+
constructor(services: LikeC4Services);
|
|
15
|
+
create(options?: ServerOptions): McpServer;
|
|
16
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
3
|
+
import { LikeC4MCPTools } from "./LikeC4MCPTools.js";
|
|
4
|
+
export class NoopLikeC4MCPServer {
|
|
5
|
+
start(port) {
|
|
6
|
+
return Promise.reject(new Error("Not implemented"));
|
|
7
|
+
}
|
|
8
|
+
stop() {
|
|
9
|
+
return Promise.reject(new Error("Not implemented"));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function toolResponse(text) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: "text",
|
|
16
|
+
text
|
|
17
|
+
}]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export class LikeC4MCPServerFactory {
|
|
21
|
+
constructor(services) {
|
|
22
|
+
this.services = services;
|
|
23
|
+
}
|
|
24
|
+
create(options) {
|
|
25
|
+
const {
|
|
26
|
+
instructions,
|
|
27
|
+
listProjects,
|
|
28
|
+
readProjectSummary,
|
|
29
|
+
searchElement,
|
|
30
|
+
readElement,
|
|
31
|
+
readView
|
|
32
|
+
} = LikeC4MCPTools;
|
|
33
|
+
const mcp = new McpServer({
|
|
34
|
+
name: "LikeC4",
|
|
35
|
+
version: packageJson.version
|
|
36
|
+
}, {
|
|
37
|
+
instructions,
|
|
38
|
+
...options,
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: {},
|
|
41
|
+
...options?.capabilities
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const tools = this.services.mcp.Tools;
|
|
45
|
+
mcp.tool(
|
|
46
|
+
listProjects.name,
|
|
47
|
+
listProjects.description,
|
|
48
|
+
async () => {
|
|
49
|
+
return toolResponse(await tools.listProjects());
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
mcp.tool(
|
|
53
|
+
readProjectSummary.name,
|
|
54
|
+
readProjectSummary.description,
|
|
55
|
+
readProjectSummary.paramsSchema,
|
|
56
|
+
async (params) => {
|
|
57
|
+
return toolResponse(await tools.readProjectSummary(params.project));
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
mcp.tool(
|
|
61
|
+
searchElement.name,
|
|
62
|
+
searchElement.description,
|
|
63
|
+
searchElement.paramsSchema,
|
|
64
|
+
async (params) => {
|
|
65
|
+
return toolResponse(await tools.searchElement(params));
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
mcp.tool(
|
|
69
|
+
readElement.name,
|
|
70
|
+
readElement.description,
|
|
71
|
+
readElement.paramsSchema,
|
|
72
|
+
async (params) => {
|
|
73
|
+
return toolResponse(await tools.readElement(params));
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
mcp.tool(
|
|
77
|
+
readView.name,
|
|
78
|
+
readView.description,
|
|
79
|
+
readView.paramsSchema,
|
|
80
|
+
async (params) => {
|
|
81
|
+
return toolResponse(await tools.readView(params));
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
return mcp;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type ProjectId } from '@likec4/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { LikeC4Services } from '../module';
|
|
4
|
+
export declare namespace LikeC4MCPTools {
|
|
5
|
+
const instructions = "This server provides access to LikeC4 model.\n\nKey capabilities:\n- List all available LikeC4 projects in the workspace\n- Search for LikeC4 project and return its summary, that includes specifications, all elements and views\n- Search for LikeC4 element by title\n- Read details about LikeC4 element by id\n- Read details about LikeC4 view by id\n\n";
|
|
6
|
+
const listProjects: {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
};
|
|
10
|
+
const readProjectSummary: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
paramsSchema: {
|
|
14
|
+
project: z.ZodOptional<z.ZodString>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const searchElement: {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
paramsSchema: {
|
|
21
|
+
search: z.ZodString;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const readElement: {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
paramsSchema: {
|
|
28
|
+
id: z.ZodString;
|
|
29
|
+
project: z.ZodOptional<z.ZodString>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const readView: {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
paramsSchema: {
|
|
36
|
+
id: z.ZodString;
|
|
37
|
+
project: z.ZodOptional<z.ZodString>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface LikeC4MCPTools {
|
|
42
|
+
listProjects(): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Searches for LikeC4 project and returns its summary, specifications, elements and views
|
|
45
|
+
*
|
|
46
|
+
* @param project Project name (optional, will use default project if not specified)
|
|
47
|
+
*/
|
|
48
|
+
readProjectSummary(project?: string): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Searches for LikeC4 elements that have the search string in their names
|
|
51
|
+
* Can be used to resolve projects for further requests (like read-element or read-project-summary)
|
|
52
|
+
*
|
|
53
|
+
* @param params.search non-empty string
|
|
54
|
+
*/
|
|
55
|
+
searchElement(params: {
|
|
56
|
+
search: string;
|
|
57
|
+
}): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Read details about LikeC4 element.
|
|
60
|
+
*
|
|
61
|
+
* @param params.id Element id (FQN)
|
|
62
|
+
* @param params.project Project name (optional, will use default project if not specified)
|
|
63
|
+
*/
|
|
64
|
+
readElement(params: {
|
|
65
|
+
id: string;
|
|
66
|
+
project?: string | undefined;
|
|
67
|
+
}): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Read details about LikeC4 view.
|
|
70
|
+
*
|
|
71
|
+
* @param params.id View id (FQN)
|
|
72
|
+
* @param params.project Project name (optional, will use default project if not specified)
|
|
73
|
+
*/
|
|
74
|
+
readView(params: {
|
|
75
|
+
id: string;
|
|
76
|
+
project?: string | undefined;
|
|
77
|
+
}): Promise<string>;
|
|
78
|
+
}
|
|
79
|
+
export declare class DefaultLikeC4MCPTools implements LikeC4MCPTools {
|
|
80
|
+
private services;
|
|
81
|
+
private readonly languageServices;
|
|
82
|
+
constructor(services: LikeC4Services);
|
|
83
|
+
listProjects(): Promise<string>;
|
|
84
|
+
readProjectSummary(_project?: ProjectId): Promise<string>;
|
|
85
|
+
searchElement(params: {
|
|
86
|
+
search: string;
|
|
87
|
+
}): Promise<string>;
|
|
88
|
+
readElement(params: {
|
|
89
|
+
id: string;
|
|
90
|
+
project?: string;
|
|
91
|
+
}): Promise<string>;
|
|
92
|
+
readView(params: {
|
|
93
|
+
id: string;
|
|
94
|
+
project?: string;
|
|
95
|
+
}): Promise<string>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { loggable } from "@likec4/log";
|
|
2
|
+
import { flatMap } from "remeda";
|
|
3
|
+
import stripIndent from "strip-indent";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { logger as mainLogger } from "../logger.js";
|
|
6
|
+
import { toSingleLine } from "../model/parser/Base.js";
|
|
7
|
+
import { ProjectsManager } from "../workspace/ProjectsManager.js";
|
|
8
|
+
import { elementResource, modelViewResource } from "./utils.js";
|
|
9
|
+
const logger = mainLogger.getChild("LikeC4MCPServices");
|
|
10
|
+
function singleLine(str) {
|
|
11
|
+
const res = toSingleLine(str)?.replaceAll('"', "'");
|
|
12
|
+
return res ? `"${res}"` : "null";
|
|
13
|
+
}
|
|
14
|
+
function outputEach(iterator, ifEmpty, output) {
|
|
15
|
+
const items = [...iterator];
|
|
16
|
+
if (items.length === 0) {
|
|
17
|
+
return [ifEmpty];
|
|
18
|
+
}
|
|
19
|
+
return flatMap(items, output);
|
|
20
|
+
}
|
|
21
|
+
export var LikeC4MCPTools;
|
|
22
|
+
((LikeC4MCPTools2) => {
|
|
23
|
+
LikeC4MCPTools2.instructions = `This server provides access to LikeC4 model.
|
|
24
|
+
|
|
25
|
+
Key capabilities:
|
|
26
|
+
- List all available LikeC4 projects in the workspace
|
|
27
|
+
- Search for LikeC4 project and return its summary, that includes specifications, all elements and views
|
|
28
|
+
- Search for LikeC4 element by title
|
|
29
|
+
- Read details about LikeC4 element by id
|
|
30
|
+
- Read details about LikeC4 view by id
|
|
31
|
+
|
|
32
|
+
`;
|
|
33
|
+
LikeC4MCPTools2.listProjects = {
|
|
34
|
+
name: "list-projects",
|
|
35
|
+
description: "Lists all available LikeC4 projects in the workspace"
|
|
36
|
+
};
|
|
37
|
+
LikeC4MCPTools2.readProjectSummary = {
|
|
38
|
+
name: "read-project-summary",
|
|
39
|
+
description: stripIndent(`
|
|
40
|
+
Searches for LikeC4 project and returns its summary, specifications, elements and views
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
project: Project name
|
|
44
|
+
`),
|
|
45
|
+
paramsSchema: {
|
|
46
|
+
project: z.string().optional()
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
LikeC4MCPTools2.searchElement = {
|
|
50
|
+
name: "search-element",
|
|
51
|
+
description: stripIndent(`
|
|
52
|
+
Search for LikeC4 elements that have the search string in their names
|
|
53
|
+
Can be used to resolve projects for further requests (like read-element or read-project-summary)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
search: non-empty string
|
|
57
|
+
`),
|
|
58
|
+
paramsSchema: {
|
|
59
|
+
search: z.string()
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
LikeC4MCPTools2.readElement = {
|
|
63
|
+
name: "read-element",
|
|
64
|
+
description: stripIndent(`
|
|
65
|
+
Read details about a LikeC4 element
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
id: Element id (FQN)
|
|
69
|
+
project: Project name (optional, will use default project if not specified)
|
|
70
|
+
`),
|
|
71
|
+
paramsSchema: {
|
|
72
|
+
id: z.string().min(1),
|
|
73
|
+
project: z.string().optional()
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
LikeC4MCPTools2.readView = {
|
|
77
|
+
name: "read-view",
|
|
78
|
+
description: stripIndent(`
|
|
79
|
+
Read details about a LikeC4 view
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
id: View id
|
|
83
|
+
project: Project name (optional, will use default project if not specified)
|
|
84
|
+
`),
|
|
85
|
+
paramsSchema: {
|
|
86
|
+
id: z.string().min(1),
|
|
87
|
+
project: z.string().optional()
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
})(LikeC4MCPTools || (LikeC4MCPTools = {}));
|
|
91
|
+
export class DefaultLikeC4MCPTools {
|
|
92
|
+
constructor(services) {
|
|
93
|
+
this.services = services;
|
|
94
|
+
this.languageServices = services.likec4.LanguageServices;
|
|
95
|
+
}
|
|
96
|
+
languageServices;
|
|
97
|
+
async listProjects() {
|
|
98
|
+
const projects = await this.languageServices.projects();
|
|
99
|
+
const response = [];
|
|
100
|
+
for (const project of projects) {
|
|
101
|
+
if (!project.documents) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
response.push(
|
|
105
|
+
`<likec4project>`,
|
|
106
|
+
`id: "${project.id}"`,
|
|
107
|
+
`folder: ${project.folder.toString()}`,
|
|
108
|
+
"sources:",
|
|
109
|
+
...project.documents.map((d) => `- ${d.toString()}`),
|
|
110
|
+
""
|
|
111
|
+
);
|
|
112
|
+
try {
|
|
113
|
+
const model = await this.languageServices.computedModel(project.id);
|
|
114
|
+
const elements = [...model.elements()];
|
|
115
|
+
if (elements.length > 0) {
|
|
116
|
+
response.push(
|
|
117
|
+
"elements:",
|
|
118
|
+
...elements.flatMap((el) => [
|
|
119
|
+
`- id: ${el.id}`,
|
|
120
|
+
` kind: ${el.kind}`,
|
|
121
|
+
` title: ${singleLine(el.title)}`,
|
|
122
|
+
""
|
|
123
|
+
]),
|
|
124
|
+
""
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const views = [...model.views()];
|
|
128
|
+
if (views.length > 0) {
|
|
129
|
+
response.push(
|
|
130
|
+
"views:",
|
|
131
|
+
...views.flatMap((v) => [
|
|
132
|
+
`- id: ${v.id}`,
|
|
133
|
+
` title: ${singleLine(v.title)}`,
|
|
134
|
+
""
|
|
135
|
+
]),
|
|
136
|
+
""
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error(loggable(error));
|
|
141
|
+
}
|
|
142
|
+
response.push(`</likec4project>`);
|
|
143
|
+
}
|
|
144
|
+
if (response.length === 0) {
|
|
145
|
+
response.push(
|
|
146
|
+
`<likec4project>`,
|
|
147
|
+
`id: "default"`,
|
|
148
|
+
`folder: ${this.languageServices.workspaceUri.toString()}`,
|
|
149
|
+
`</likec4project>`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return response.join("\n");
|
|
153
|
+
}
|
|
154
|
+
async readProjectSummary(_project) {
|
|
155
|
+
const projectId = _project ?? ProjectsManager.DefaultProjectId;
|
|
156
|
+
const project = this.languageServices.projects().find((p) => p.id === projectId);
|
|
157
|
+
if (!project) {
|
|
158
|
+
return "Project not found";
|
|
159
|
+
}
|
|
160
|
+
const model = await this.languageServices.computedModel(project.id);
|
|
161
|
+
const response = [
|
|
162
|
+
`project: "${project.id}"`,
|
|
163
|
+
`folder: ${project.folder.toString()}`
|
|
164
|
+
];
|
|
165
|
+
if (project.documents) {
|
|
166
|
+
response.push(
|
|
167
|
+
"sources:",
|
|
168
|
+
...project.documents.map((d) => `- ${d.toString()}`),
|
|
169
|
+
""
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
response.push("<specifications>");
|
|
173
|
+
const elementKinds = Object.keys(model.$model.specification.elements);
|
|
174
|
+
if (elementKinds.length > 0) {
|
|
175
|
+
response.push(
|
|
176
|
+
"element kinds:",
|
|
177
|
+
...elementKinds.map((kind) => `- ${kind}`),
|
|
178
|
+
""
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const relationshipKinds = Object.keys(model.$model.specification.relationships);
|
|
182
|
+
if (relationshipKinds.length > 0) {
|
|
183
|
+
response.push(
|
|
184
|
+
"relationship kinds:",
|
|
185
|
+
...relationshipKinds.map((kind) => `- ${kind}`),
|
|
186
|
+
""
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const deploymentKinds = Object.keys(model.$model.specification.deployments);
|
|
190
|
+
if (deploymentKinds.length > 0) {
|
|
191
|
+
response.push(
|
|
192
|
+
"deployment node kinds:",
|
|
193
|
+
...deploymentKinds.map((kind) => `- ${kind}`),
|
|
194
|
+
""
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (model.allTags().length > 0) {
|
|
198
|
+
response.push(
|
|
199
|
+
"tags:",
|
|
200
|
+
...model.allTags().map((t) => `- ${t}`),
|
|
201
|
+
""
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
response.push(
|
|
205
|
+
"</specifications>",
|
|
206
|
+
""
|
|
207
|
+
);
|
|
208
|
+
response.push(
|
|
209
|
+
"<elements>",
|
|
210
|
+
...outputEach(model.elements(), "No elements", (el) => [
|
|
211
|
+
`- id: ${el.id}`,
|
|
212
|
+
...el.parent ? [
|
|
213
|
+
` parentId: ${el.parent.id}`
|
|
214
|
+
] : [],
|
|
215
|
+
` kind: ${el.kind}`,
|
|
216
|
+
` shape: ${el.shape}`,
|
|
217
|
+
` title: ${singleLine(el.title)}`,
|
|
218
|
+
` description: ${singleLine(el.description)}`,
|
|
219
|
+
` technology: ${singleLine(el.technology)}`,
|
|
220
|
+
` tags: ${JSON.stringify(el.tags)}`,
|
|
221
|
+
""
|
|
222
|
+
]),
|
|
223
|
+
"</elements>",
|
|
224
|
+
"<views>",
|
|
225
|
+
...outputEach(model.views(), "No views", (v) => [
|
|
226
|
+
`- id: ${v.id}`,
|
|
227
|
+
` viewType: ${v.__}`,
|
|
228
|
+
` title: ${singleLine(v.title)}`,
|
|
229
|
+
""
|
|
230
|
+
]),
|
|
231
|
+
"</views>"
|
|
232
|
+
);
|
|
233
|
+
return response.join("\n");
|
|
234
|
+
}
|
|
235
|
+
async searchElement(params) {
|
|
236
|
+
const search = params.search.toLowerCase();
|
|
237
|
+
const found = [];
|
|
238
|
+
for (const project of this.languageServices.projects()) {
|
|
239
|
+
try {
|
|
240
|
+
const model = await this.languageServices.computedModel(project.id);
|
|
241
|
+
const elements = [...model.elements()].filter((el) => el.title.toLowerCase().includes(search));
|
|
242
|
+
if (elements.length > 0) {
|
|
243
|
+
found.push(
|
|
244
|
+
"<project>",
|
|
245
|
+
`project: "${project.id}"`,
|
|
246
|
+
"found:",
|
|
247
|
+
...elements.flatMap((el) => [
|
|
248
|
+
`- id: ${el.id}`,
|
|
249
|
+
` kind: ${el.kind}`,
|
|
250
|
+
` title: ${singleLine(el.title)}`,
|
|
251
|
+
""
|
|
252
|
+
]),
|
|
253
|
+
"</project>"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
logger.error(loggable(error));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return found.length > 0 ? found.join("\n") : "No elements with this name found";
|
|
261
|
+
}
|
|
262
|
+
async readElement(params) {
|
|
263
|
+
const projectId = params.project ?? ProjectsManager.DefaultProjectId;
|
|
264
|
+
const project = this.languageServices.projects().find((p) => p.id === projectId);
|
|
265
|
+
if (!project) {
|
|
266
|
+
return `Project "${projectId}" not found`;
|
|
267
|
+
}
|
|
268
|
+
const model = await this.languageServices.computedModel(project.id);
|
|
269
|
+
const element = model.findElement(params.id);
|
|
270
|
+
if (!element) {
|
|
271
|
+
return `Element "${params.id}" not found in project "${projectId}"`;
|
|
272
|
+
}
|
|
273
|
+
return JSON.stringify(elementResource(this.languageServices, element, project.id));
|
|
274
|
+
}
|
|
275
|
+
async readView(params) {
|
|
276
|
+
const projectId = params.project ?? ProjectsManager.DefaultProjectId;
|
|
277
|
+
const project = this.languageServices.projects().find((p) => p.id === projectId);
|
|
278
|
+
if (!project) {
|
|
279
|
+
return `Project "${projectId}" not found`;
|
|
280
|
+
}
|
|
281
|
+
const model = await this.languageServices.computedModel(project.id);
|
|
282
|
+
const view = model.findView(params.id);
|
|
283
|
+
if (!view) {
|
|
284
|
+
return `View "${params.id}" not found in project "${projectId}"`;
|
|
285
|
+
}
|
|
286
|
+
return JSON.stringify(modelViewResource(this.languageServices, view, project.id));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AsyncDisposable } from 'langium';
|
|
2
|
+
import type { LikeC4Services } from '../../module';
|
|
3
|
+
import type { LikeC4MCPServer } from '../LikeC4MCPServerFactory';
|
|
4
|
+
export declare class SSELikeC4MCPServer implements LikeC4MCPServer, AsyncDisposable {
|
|
5
|
+
private services;
|
|
6
|
+
private readonly transports;
|
|
7
|
+
private server;
|
|
8
|
+
private port;
|
|
9
|
+
constructor(services: LikeC4Services);
|
|
10
|
+
dispose(): Promise<void>;
|
|
11
|
+
start(port?: number): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { logger as mainLogger } from "../../logger.js";
|
|
4
|
+
const logger = mainLogger.getChild("LikeC4MCPServer");
|
|
5
|
+
export class SSELikeC4MCPServer {
|
|
6
|
+
constructor(services) {
|
|
7
|
+
this.services = services;
|
|
8
|
+
}
|
|
9
|
+
// Store transports by session ID to send notifications
|
|
10
|
+
transports = {};
|
|
11
|
+
server = void 0;
|
|
12
|
+
port = 33335;
|
|
13
|
+
async dispose() {
|
|
14
|
+
await this.stop();
|
|
15
|
+
}
|
|
16
|
+
async start(port = 33335) {
|
|
17
|
+
if (this.server) {
|
|
18
|
+
if (this.port === port) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await this.stop();
|
|
22
|
+
}
|
|
23
|
+
logger.info("Starting server on port {port}", { port });
|
|
24
|
+
this.port = port;
|
|
25
|
+
const mcp = this.services.mcp.ServerFactory.create();
|
|
26
|
+
const app = express();
|
|
27
|
+
app.get("/sse", async (_, res) => {
|
|
28
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
29
|
+
this.transports[transport.sessionId] = transport;
|
|
30
|
+
logger.debug`SSE connection established, sessionId: ${transport.sessionId}`;
|
|
31
|
+
res.on("close", () => {
|
|
32
|
+
delete this.transports[transport.sessionId];
|
|
33
|
+
});
|
|
34
|
+
await mcp.connect(transport);
|
|
35
|
+
});
|
|
36
|
+
app.post("/messages", async (req, res) => {
|
|
37
|
+
const sessionId = req.query["sessionId"];
|
|
38
|
+
const transport = this.transports[sessionId];
|
|
39
|
+
if (transport) {
|
|
40
|
+
logger.debug`SSE message received, sessionId: ${sessionId}`;
|
|
41
|
+
await transport.handlePostMessage(req, res);
|
|
42
|
+
} else {
|
|
43
|
+
res.status(400).send("No transport found for sessionId");
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
this.server = app.listen(port, (err) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
logger.error("Failed to start server", { err });
|
|
50
|
+
reject(err);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
logger.info("server listening on port {port}", { port });
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async stop() {
|
|
59
|
+
const server = this.server;
|
|
60
|
+
if (!server) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
logger.info("Stopping server");
|
|
64
|
+
this.server = void 0;
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
server.close((err) => {
|
|
67
|
+
if (err) {
|
|
68
|
+
logger.error("Failed to stop SSE server", { err });
|
|
69
|
+
}
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|