Skip to content

Commit 80d7ac7

Browse files
authored
Merge pull request #724 from GraphScope/docs-0310
feat: Add a mcp demo for the portal.
2 parents 362934f + 1771f80 commit 80d7ac7

File tree

9 files changed

+1079
-92
lines changed

9 files changed

+1079
-92
lines changed

docs/interactive/components/Home/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ gsctl instance deploy --type interactive
6969
{
7070
type: 'docker',
7171
description: `# Pull the GraphScope Interactive Docker image
72-
docker pull registry.cn-hongkong.aliyuncs.com/graphscope/interactive:0.29.3-arm64
72+
docker pull registry.cn-hongkong.aliyuncs.com/graphscope/interactive
7373
7474
# Start the GraphScope Interactive service
75-
docker run -d --name gs --label flex=interactive -p 8080:8080 -p 7777:7777 -p 10000:10000 -p 7687:7687 registry.cn-hongkong.aliyuncs.com/graphscope/interactive:0.29.3-arm64 --enable-coordinator
75+
docker run -d --name gs -p 8080:8080 -p 7777:7777 -p 10000:10000 -p 7687:7687 registry.cn-hongkong.aliyuncs.com/graphscope/interactive --enable-coordinator --port-mapping "8080:8080,7777:7777,10000:10000,7687:7687"
7676
`,
7777
},
7878
];

examples/mcp-portal/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "mcp-portal",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"prestart": "tsc",
9+
"start": "node build/sse.js"
10+
},
11+
"dependencies": {
12+
"@graphscope/studio-driver": "workspace:*",
13+
"@modelcontextprotocol/sdk": "^1.6.0",
14+
"@zodios/core": "^10.9.6",
15+
"express": "^4.21.2",
16+
"zod": "^3.24.2"
17+
},
18+
"devDependencies": {
19+
"@types/express": "^5.0.0",
20+
"@types/node": "^22.13.5",
21+
"typescript": "^5.7.3"
22+
},
23+
"keywords": [],
24+
"author": "",
25+
"license": "ISC",
26+
"publishConfig": {
27+
"access": "public",
28+
"registry": "https://registry.npmjs.org/"
29+
}
30+
}

examples/mcp-portal/src/helpers.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { CypherDriver } from "@graphscope/studio-driver";
2+
const DriverMap = new Map();
3+
4+
export const getDriver = () => {
5+
const endpoint = "neo4j://127.0.0.1:7687";
6+
const username = "admin";
7+
const password = "admin";
8+
const id = `${endpoint}:${username}:${password}`;
9+
if (!DriverMap.has(id)) {
10+
DriverMap.set(id, new CypherDriver(endpoint, username, password));
11+
}
12+
return DriverMap.get(id);
13+
};
14+
15+
export function colorize(text: any, colorCode: number) {
16+
return `\x1b[${colorCode}m${text}\x1b[0m`;
17+
}
18+
19+
export const models = [
20+
{
21+
name: "qwen-plus",
22+
endpoint:
23+
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
24+
},
25+
{
26+
name: "deepseek-chat",
27+
endpoint: "https://api.deepseek.com/chat/completions",
28+
},
29+
{
30+
name: "gpt-4o-mini",
31+
endpoint: "https://api.openai.com/v1/chat/completions",
32+
},
33+
];
34+
35+
interface Base {
36+
status?: "pending" | "success" | "cancel";
37+
role?: "system" | "assistant" | "user";
38+
content?: string;
39+
timestamp?: number;
40+
reserved?: boolean;
41+
}
42+
43+
export class Message implements Base {
44+
status: "pending" | "success" | "cancel";
45+
46+
role: "system" | "assistant" | "user";
47+
48+
content: string;
49+
50+
timestamp: number;
51+
52+
reserved: boolean;
53+
54+
constructor(props: Partial<Base>) {
55+
this.status = props.status || "pending";
56+
this.role = props.role || "user";
57+
this.content = props.content || "";
58+
this.timestamp = props.timestamp || Date.now();
59+
this.reserved = props.reserved || false;
60+
}
61+
}
62+
63+
export function query(
64+
messages: Message[],
65+
callback?: (message: { content: string }) => void,
66+
signal?: AbortSignal
67+
): Promise<{
68+
status: "success" | "cancel" | "failed";
69+
message: any;
70+
}> {
71+
const model = models[0].name;
72+
const { endpoint, name } = models.find((m) => m.name === model) || models[0];
73+
const apiKey = process.env.API_KEY;
74+
return fetch(endpoint, {
75+
signal,
76+
method: "POST",
77+
headers: {
78+
"Content-Type": "application/json",
79+
Authorization: `Bearer ${apiKey}`,
80+
},
81+
body: JSON.stringify({
82+
model: name,
83+
stream: false, // 启用流式返回
84+
messages: messages.map(({ role, content }) => ({ role, content })),
85+
}),
86+
})
87+
.then((response) => {
88+
const message = {
89+
content: "",
90+
complete: false,
91+
};
92+
// 返回流和 Promise
93+
const stream = response.body;
94+
if (!stream) {
95+
return {
96+
status: "failed",
97+
message: { content: "Response body is missing" },
98+
};
99+
}
100+
const reader = response.body.getReader();
101+
// 处理流式数据
102+
//@ts-ignore
103+
function processStream(params: any) {
104+
const { done, value } = params;
105+
message.complete = false;
106+
if (done) {
107+
message.complete = true;
108+
return {
109+
status: "success",
110+
message: message,
111+
};
112+
}
113+
const chunk = new TextDecoder().decode(value);
114+
chunk.split("\n").forEach((line) => {
115+
if (line.startsWith("data: ")) {
116+
const data = line.slice(6);
117+
if (data === "[DONE]") {
118+
message.complete = true;
119+
return {
120+
status: "success",
121+
message: message,
122+
};
123+
}
124+
const parsedData = JSON.parse(data);
125+
if (parsedData.choices[0].delta.content !== undefined) {
126+
message.content =
127+
message.content + parsedData.choices[0].delta.content;
128+
}
129+
}
130+
});
131+
callback && callback(message);
132+
// 继续读取流
133+
return reader.read().then(processStream);
134+
}
135+
return reader.read().then(processStream);
136+
})
137+
.catch((error) => {
138+
if (error.name === "AbortError")
139+
return {
140+
status: "cancel",
141+
message: null,
142+
};
143+
return {
144+
status: "failed",
145+
message: error.message,
146+
};
147+
});
148+
}

examples/mcp-portal/src/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3+
4+
import tools from "./tools/index";
5+
import resources from "./resource";
6+
7+
const server = new McpServer({
8+
name: "portal-mcp",
9+
version: "1.0.0",
10+
});
11+
12+
/** register tools */
13+
Object.values(tools).forEach((tool) => {
14+
//@ts-ignore
15+
server.tool(tool.name, tool.description, tool.parameters, tool.execute);
16+
});
17+
18+
/** register resources */
19+
Object.values(resources).forEach((r) => {
20+
server.resource(r.name, r.uri, r.execute);
21+
});
22+
23+
async function main() {
24+
const transport = new StdioServerTransport();
25+
await server.connect(transport);
26+
console.error(
27+
"Portal MCP Server running on stdio",
28+
process.env.DEEPSEEK_API_KEY
29+
);
30+
}
31+
32+
main().catch((error) => {
33+
console.error("Fatal error in main():", error);
34+
process.exit(1);
35+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getDriver } from "../helpers";
2+
const schema = {
3+
name: "graph-schema",
4+
uri: "schema://main",
5+
execute: async (uri: URL) => {
6+
const driver = getDriver();
7+
try {
8+
const result = await driver.query("call gs.procedure.meta.schema()");
9+
const { schema } = result.table[0];
10+
11+
return {
12+
contents: [
13+
{
14+
uri: uri.href,
15+
text: schema,
16+
},
17+
],
18+
};
19+
} catch (error: any) {
20+
return {
21+
contents: [
22+
{
23+
uri: uri.href,
24+
text: `xxxx: ${error.message}`,
25+
},
26+
],
27+
};
28+
}
29+
},
30+
};
31+
32+
export default { schema };

examples/mcp-portal/src/sse.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import express from "express";
2+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4+
5+
import tools from "./tools/index";
6+
import resources from "./resource";
7+
8+
const server = new McpServer({
9+
name: "portal-mcp",
10+
version: "1.0.0",
11+
});
12+
13+
/** register tools */
14+
Object.values(tools).forEach((tool) => {
15+
//@ts-ignore
16+
server.tool(tool.name, tool.description, tool.parameters, tool.execute);
17+
});
18+
19+
/** register resources */
20+
Object.values(resources).forEach((r) => {
21+
server.resource(r.name, r.uri, r.execute);
22+
});
23+
24+
const app = express();
25+
let transport: SSEServerTransport;
26+
app.get("/sse", async (req, res) => {
27+
transport = new SSEServerTransport("/messages", res);
28+
await server.connect(transport);
29+
});
30+
31+
app.post("/messages", async (req, res) => {
32+
await transport.handlePostMessage(req, res);
33+
});
34+
35+
app.listen(3001, () => [console.log("Server listening on port 3001")]);

0 commit comments

Comments
 (0)