Salesforce与Neo4j集成工具

通过MCP、RAG和LWC三种架构实现Salesforce与Neo4j图数据库的交互,支持自然语言查询、Cypher查询和智能RAG翻译。

  • by belavaditech
  • JavaScript
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 belavaditech/salesforce-mcp-neo4j-demo

使用说明

🚀 使用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上执行它。


✅ 方法2 – 使用GraphRag检索器的RAG(检索增强生成)

Salesforce → Grounding → Node.js Gateway → GraphRAG Retriever → Neo4j

在这里,我们不是直接将自然语言(NL)翻译成Cypher,而是使用 GraphRAG Text2CypherRetriever它返回 上下文感知的Cypher建议 与;和;带着 top_k 执行前的排名。


✅ 方法3 – 直接执行Cypher查询(无RAG模式)

Salesforce → Node.js Gateway → MCP-Neo4j-Cypher → Neo4j

这是 开发者模式我们 **绕过接地和RAG(快速访问指南/风险评估小组/其他根据上下文确定的缩写)**Salesforce 直接提交 Cypher 查询,通过 MCP 进行路由。


🏗 架构概述

层 组件
用户界面层 闪电网络组件(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

🛠 已实现的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<String, Object> payload = new Map<String, Object>{ '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-rag POST(发布)并转发到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)

<template>
  <lightning-card title="Salesforce RAG Gateway Demo">
    <div class="slds-p-around_medium">
      <lightning-input label="Your Question" value={userInput} onchange={handleInput}></lightning-input>
      <lightning-radio-group
        name="mode"
        label="Choose Mode"
        options={modeOptions}
        value={selectedMode}
        onchange={handleModeChange}>
      </lightning-radio-group>
      <lightning-button label="Submit" onclick={handleSubmit} class="slds-m-top_medium"></lightning-button>
      <template if:true={response}>
        <p class="slds-m-top_medium"><strong>Response:</strong></p>
        <pre>{response}</pre>
      </template>
    </div>
  </lightning-card>
</template>

📸 截图

  • 📷 带有3个选项卡的LWC(Lightweight Component,轻量级组件)

🔮 总结思考

这个实验证明了 Salesforce不再仅仅是一个CRM(客户关系管理)平台 – 它可以 通过MCP和Neo4j实现图智能调度. 随着 GraphRAG, 基于实际情境的密码生成,以及 结构化MCP工具我们可以将Salesforce转变为一个 图感知智能控制台。