🚀 使用MCP、RAG和LWC将Salesforce与Neo4j集成——三种架构的实用演示
在这个演示中,我展示了 三种不同的架构 哪里 Salesforce与Neo4j图数据库进行交互 使用 现代上下文协议(MCP), GraphRAG,和 自定义 Node.js 网关逻辑。
我的目标很简单:\ 👉 *将Neo4j作为知识图谱后端暴露给Salesforce* 以一种开发者可以使用(某种方式)查询图数据的方式 自然语言, Cypher(密码/暗语)或者 智能RAG翻译.
______________________________________________________________________
🎯 三种整合方法
✅ 表示正确、确认或成功。 方法1 – 使用直接MCP-Neo4j-Cypher的RAG(检索增强生成)
Salesforce → Grounding → Node.js Gateway → MCP-Neo4j-Cypher → Neo4j在这种方法中,Salesforce发送一个 自然语言请求. 那个 MCP工具将其转换为Cypher 使用接地逻辑并直接在Neo4j上执行它。
Method 1 – RAG with Direct MCP-Neo4j-Cypher
______________________________________________________________________
✅ 方法2 – 使用GraphRag检索器的RAG(检索增强生成)
Salesforce → Grounding → Node.js Gateway → GraphRAG Retriever → Neo4j在这里,我们不是直接将自然语言(NL)翻译成Cypher,而是使用 GraphRAG Text2CypherRetriever它返回 上下文感知的Cypher建议 与;和;带着 top_k 执行前的排名。
Method 2 – RAG with GraphRag Retriever
______________________________________________________________________
✅ 方法3 – 直接执行Cypher查询(无RAG模式)
Salesforce → Node.js Gateway → MCP-Neo4j-Cypher → Neo4j这是 *开发者模式*我们 绕过接地和RAG(快速访问指南/风险评估小组/其他根据上下文确定的缩写)Salesforce 直接提交 Cypher 查询,通过 MCP 进行路由。
Method 3 – Direct Cypher Execution (No-RAG Mode)
______________________________________________________________________
🏗 架构概述
| 层 | 组件 |
|---|---|
| 用户界面层 | 闪电网络组件(LWC) |
| 后端(Salesforce) | Apex 控制器 → 命名凭据 → HTTP 调用 |
| 网关 | Node.js MCP 客户端网关(端口 9005) |
| MCP服务 | Python MCP 服务器(端口 8005) 暴露;揭露 text2cypher 并且 read-cypher |
| 数据层 | Neo4j 图数据库 |
- LWC 发送请求 → Apex
- Apex 使用 命名凭据 + ngrok HTTPS 端点
- Node.js网关将Salesforce的HTTP请求转换为MCP请求
- Python MCP 服务器托管用于翻译或执行 Cypher 的工具
______________________________________________________________________
🎛 为什么选择Apex + 命名凭据?
Salesforce(通常译为“赛乐孚”或直接音译为“萨尔夫斯”,但更常见的可能是根据其品牌特性采用意译或保持原名,因“Salesforce”在中文语境下常被直接使用,表示该公司的产品或服务,如“销售云”等,具体翻译需结合上下文) 不允许LWC直接进行HTTP调用 由于安全策略,所以:
- Apex 发出了呼叫(或提示)
- 命名凭证暴露了网关URL
- Ngrok 提供临时 HTTPS URL,因为已命名凭据 不接受HTTP
Named credential to access Gateway)
🛠 已实现的MCP工具(Python)
# -----------------------
# Tool: text2cypher (GraphRAG)
# -----------------------
@mcp.tool(name="text2cypher")
def text2cypher_tool(query: str, top_k: int = 3):
"""
Use Neo4j GraphRAG Text2CypherRetriever to generate Cypher via rag.search().
"""
try:
# ✅ Call correct official API for latest neo4j-graphrag
result = rag.search(query_text=query)
# The retriever can also be used without using graphRag
# result = retriever.search(query_text=query)
print (result)
# fallback if result.cypher_query is missing
cypher_query = (
getattr(result, "cypher_query", None)
or getattr(result, "cypher", None)
or getattr(result, "query", None)
or (getattr(result, "metadata", {}) or {}).get("cypher")
or (getattr(result, "metadata", {}) or {}).get("cypher_text")
)
# ✅ Extract fields safely
graph_data = getattr(result, "records", None)
if not cypher_query:
return {"error": "No Cypher generated", "raw": str(result)}
# ✅ Execute Cypher via standard Neo4j session
with driver.session() as session:
data_rows = session.run(cypher_query)
final_data = [record.data() for record in data_rows]
return {
"input": query,
"cypher": cypher_query,
"graphData": final_data
}
except Exception as e:
return {"error": str(e)}
# -----------------------
# Helpers
# -----------------------
def _execute_cypher_return_rows(cypher_text: str, params: dict | None = None):
"""Execute a cypher string in read mode and return list of record.data() dicts."""
params = params or {}
with driver.session(default_access_mode="READ") as session:
result = session.run(cypher_text, **params)
return [rec.data() for rec in result]
# -----------------------
# Tool: read_neo4j_cypher
# -----------------------
@mcp.tool(name="read_neo4j_cypher")
def read_neo4j_cypher(query: str):
"""
Execute read-only Cypher and return rows.
"""
try:
rows = _execute_cypher_return_rows(query)
return {"ok": True, "rows": rows}
except Exception as e:
return {"ok": False, "error": str(e), "trace": traceback.format_exc()}
______________________________________________________________________
🛠 网关已实现(使用Node.js)
// 1) Method 1: LWC -> Gateway -> translateToCypher(LM) -> MCP read_neo4j_cypher -> groundedAnswer -> return
app.post('/method1', async (req, res) => {
try {
const { naturalLanguage } = req.body;
const cypherQuery = await translateToCypher(naturalLanguage);
const client = await mcpClientPromise;
const result = await client.callTool({ name: 'read_neo4j_cypher', arguments: { query: cypherQuery } });
console.log(result);
const grounded = await groundedAnswer(naturalLanguage, result.content?.json || result.content?.text || result, cypherQuery);
res.json({ mode: 'method1', cypher: cypherQuery, rawGrounding: result.content || null, groundedAnswer: grounded });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
// 2) Method 2: LWC -> Gateway -> MCP text2cyphertool -> returns cypher + grounding -> groundedAnswer -> return
app.post('/method2', async (req, res) => {
console.log("method2 LWC -> Gateway -> MCP text2cyphertool");
try {
const { naturalLanguage } = req.body;
const client = await mcpClientPromise;
// Call the MCP tool that exposes text2cypher retriever on the Neo4j side
const toolResult = await client.callTool({ name: 'text2cypher', arguments: { query: naturalLanguage } });
console.log(toolResult);
// Expect toolResult.content to include { cypherQuery, graphData }
// const cypherQuery = toolResult.content?.json?.cypherQuery || toolResult.content?.json?.cypher || (toolResult.content?.text || '').slice(0, 2000);
const contentBlock = toolResult.content?.[0];
if (contentBlock?.type === 'text' && contentBlock.text) {
const parsed = JSON.parse(contentBlock.text);
cypherQuery = parsed.cypher || parsed.cypherQuery || parsed.metadata?.cypher;
graphData = parsed.graphData || parsed.rows || parsed.records || parsed.data;
}
console.log("cypherQuery = " + cypherQuery);
// const graphData = toolResult.content?.json?.graphData || toolResult.content?.json?.rows || toolResult.content?.text || toolResult.content || {};
console.log("graphData = " + graphData);
const grounded = await groundedAnswer(naturalLanguage, graphData, cypherQuery);
console.log("grounded = " + grounded);
res.json({ mode: 'method2', cypher: cypherQuery, rawGrounding: graphData, groundedAnswer: grounded });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
// 3) No-RAG: direct query to DB
app.post('/no-rag', async (req, res) => {
try {
const { naturalLanguage } = req.body;
const cypherQuery = await translateToCypher(naturalLanguage);
const client = await mcpClientPromise;
const result = await client.callTool({ name: 'read_neo4j_cypher', arguments: { query: cypherQuery } });
console.log(result);
res.json({ mode: 'no-rag', cypher: cypherQuery, rawGrounding: naturalLanguage, groundedAnswer: result.content || null });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
🌍 使用 cURL 进行测试
curl -X POST https://a4f561090ffb.ngrok-free.app/method2 -H "Content-Type: application/json" -d @naturaldemo.json输入JSON(naturaldemo.json)
{"naturalLanguage": "Find list of cases"}回应
curl -X POST https://a4f561090ffb.ngrok-free.app/method2 -H "Content-Type: application/json" -d @naturaldemo.json
{"mode":"method2","cypher":"MATCH (c:Case) RETURN c","rawGrounding":[{"c":{"product":"LoginApp","subject":"Timeout issue","case_id":"001","email":"john@example.com"}},{"c":{"createdDate":"2025-09-09T13:20:55.501000000+00:00","subject":"Cannot login","caseNumber":"00012345","id":"5005j00001ABC123","priority":"High","status":"Closed"}},{"c":{"createdDate":"2025-09-09T13:20:55.501000000+00:00","subject":"Payment failed","caseNumber":"00012346","id":"5005j00001ABC456","priority":"Medium","status":"In Progress"}},{"c":{"subject":"The server is crashing in weekend"}}],"groundedAnswer":"To find the list of cases from the provided grounding data, we can extract the relevant information from each case entry. The grounding data contains several cases with different attributes. Here’s a concise summary of the cases identified:\n\n1. **Case ID: 001**\n - **Product:** LoginApp\n - **Subject:** Timeout ....
...
}
______________________________________________________________________
💻 Apex 代码以访问网关(RagGatewayController)
public with sharing class RagGatewayController {
@AuraEnabled(cacheable=false)
public static Object getMcpResponse(String input, String mode) {
Http http = new Http();
HttpRequest req = new HttpRequest();
String baseUrl = 'callout:RagNeo4jGateway/'; // Replace with actual host
String endpoint = baseUrl + '/' + mode; // mode = method1, method2, no-rag
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
Map payload = new Map{ 'naturalLanguage' => input };
req.setBody(JSON.serialize(payload));
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return JSON.deserializeUntyped(res.getBody());
} else {
throw new AuraHandledException('Gateway error: ' + res.getStatus());
}
}
}
______________________________________________________________________
💻 Apex 测试代码(匿名执行)
try {
String input = 'Find suppliers in Bangalore';
String mode = 'method1'; // method2, no-rag also valid
Object result = RagGatewayController.getMcpResponse(input, mode);
System.debug('✅ Result:
' + JSON.serializePretty(result));
} catch (Exception e) {
System.debug('❌ Exception: ' + e.getMessage());
}______________________________________________________________________
🔌 网关(Node.js MCP 客户端)– 运行在 9005 端口
作为HTTP到MCP的桥梁。接收/method1,/method2,/no-ragPOST(发布)并转发到MCP服务器。
______________________________________________________________________
🐍 Python MCP 服务器 – 运行在 8005 端口
揭露;曝光 GraphRAG Text2CypherRetriever(可译为“GraphRAG 文本转Cypher检索器”) 和 只读 Cypher 执行。
______________________________________________________________________
🎨 LWC 应用 – 用户界面层
- 3个按钮 → *方法1*, *方法2*, *No-RAG 可以翻译为“无检索增强生成”或简化为“无RAG”(在特定语境下,RAG作为缩写被广泛理解时)。这里,“RAG”通常指的是“Retrieval-Augmented Generation”(检索增强生成)技术,它结合了信息检索和文本生成的能力,以提高生成内容的质量和准确性。因此,“No-RAG”意味着不使用这种检索增强生成技术*
- 自然语言输入框
- 通过网关显示来自Neo4j的JSON结果
import { LightningElement, track } from 'lwc';
import getMcpResponse from '@salesforce/apex/RagGatewayController.getMcpResponse';
export default class RagGatewayDemo extends LightningElement {
@track userInput = '';
@track selectedMode = 'method1';
@track response;
modeOptions = [
{ label: 'Method 1: (RAG) LLM → Cypher → MCP', value: 'method1' },
{ label: 'Method 2: (RAG) MCP Text2Cypher', value: 'method2' },
{ label: 'Method 3: (No-RAG) (Just cypher)', value: 'no-rag' }
];
handleInput(event) {
this.userInput = event.target.value;
}
handleModeChange(event) {
this.selectedMode = event.detail.value;
}
async handleSubmit() {
try {
const result = await getMcpResponse({ input: this.userInput, mode: this.selectedMode });
this.response = JSON.stringify(result, null, 2);
} catch (error) {
this.response = 'Error: ' + error.body.message;
}
}
}
LWC HTML(Lightweight Components 的 HTML,即轻量级组件的 HTML)
Response:
{response}
______________________________________________________________________
📸 截图
- 📷 带有3个选项卡的LWC(Lightweight Component,轻量级组件)
Method 1 Method 2 Method 3
🔮 总结思考
这个实验证明了 Salesforce不再仅仅是一个CRM(客户关系管理)平台 – 它可以 通过MCP和Neo4j实现图智能调度. 随着 GraphRAG, 基于实际情境的密码生成,以及 结构化MCP工具我们可以将Salesforce转变为一个 图感知智能控制台。
