Building an MCP server in B4J

aeric

Expert
Licensed User
Longtime User
It is possible, according to ChatGPT.
There are 2 options,
1. Stdio-based MCP server
2. TCP / WebSocket MCP server

The latter is easier.

We can connect this MCP server to our db and provide answers to the clients like ChatGPT Desktop for Windows.
For me, it sounds like we are building a jRDC2 server but we add in the feature that allow the AI enabled client apps to talk with our data.
Example, the client apps can connect to my REST API server and read my products table to get the stock quantity or generate a report based on the invoices table.
Maybe we can then use AI agent to send a low stock notification or email the customer invoices in an autonomous workflow.
 

aeric

Expert
Licensed User
Longtime User
Architecture:
B4X:
[B4J Server]
 ├─ REST API (optional)
 ├─ Database (inventory + customer)
 └─ MCP WebSocket Server
       ├─ get_products
       ├─ search_items
       ├─ check_stock
       ├─ get_customer
       ├─ create_invoice
       └─ sales_summary

ChatGPT or any MCP client can:
  • Query inventory
  • Create invoices
  • Update stock
  • Look up customer history
  • Generate reports
  • Perform backend actions
Code:
B4X:
# Project: Mini MCP WebSocket Server (B4J)
# Files included below. Copy each file into a new B4J project with the same filename.

---
File: Main.bas
' Main WebSocket MCP server
Sub Process_Globals
    Private ws As WebSocketServer
    Private ServerPort As Int = 8888
    Private store As ProductStore
End Sub

Sub AppStart (Args() As String)
    Log("Starting Mini MCP WebSocket Server...")
    store.Initialize

    ws.Initialize("ws")
    ws.Start(ServerPort, "0.0.0.0")
    Log($"🚀 MCP WebSocket server started on ws://0.0.0.0:${ServerPort}/")
End Sub

' New client connected
Sub ws_Connected (wsClient As WebSocket)
    Log("Client connected: " & wsClient.GetRemoteAddress)
    ' Send an MCP-style "initialized" capabilities message (not strictly JSON-RPC)
    Dim gen As JSONGenerator
    Dim cap As Map = CreateMap("protocolVersion": "1.0", "capabilities": CreateMap("tools": Array As String("echo","get_products","search_items","create_invoice","check_stock","get_sales_summary","get_time","get_server_info")))
    gen.Initialize(cap)
    wsClient.SendText(gen.ToString)
End Sub

Sub ws_TextMessage (wsClient As WebSocket, msg As String)
    Log("Received: " & msg)
    Dim parser As JSONParser
    parser.Initialize(msg)
    Try
        Dim req As Map = parser.NextObject
        Dim method As String = req.GetDefault("method", "")
        Dim id = req.GetDefault("id", Null)

        Select method
            Case "echo"
                Handle_Echo(wsClient, id, req)
            Case "get_products"
                Handle_GetProducts(wsClient, id, req)
            Case "search_items"
                Handle_SearchItems(wsClient, id, req)
            Case "create_invoice"
                Handle_CreateInvoice(wsClient, id, req)
            Case "check_stock"
                Handle_CheckStock(wsClient, id, req)
            Case "get_sales_summary"
                Handle_GetSalesSummary(wsClient, id, req)
            Case "get_time"
                Handle_GetTime(wsClient, id, req)
            Case "get_server_info"
                Handle_GetServerInfo(wsClient, id, req)
            Case Else
                ReplyError(wsClient, id, -32601, "Method not found: " & method)
        End Select
    Catch
        ReplyError(wsClient, Null, -32700, "Parse error or invalid JSON")
    End Try
End Sub

Sub ws_Disconnected (wsClient As WebSocket)
    Log("Client disconnected: " & wsClient.GetRemoteAddress)
End Sub

' ----------------------
' RPC helper / replies
Private Sub ReplyResult(wsClient As WebSocket, id, result As Object)
    Dim resp As Map = CreateMap("jsonrpc": "2.0", "id": id, "result": result)
    Dim gen As JSONGenerator
    gen.Initialize(resp)
    wsClient.SendText(gen.ToString)
End Sub

Private Sub ReplyError(wsClient As WebSocket, id, code As Int, message As String)
    Dim resp As Map = CreateMap("jsonrpc": "2.0")
    If id <> Null Then resp.Put("id", id)
    resp.Put("error", CreateMap("code": code, "message": message))
    Dim gen As JSONGenerator
    gen.Initialize(resp)
    wsClient.SendText(gen.ToString)
End Sub

' ----------------------
' Handlers (business / database tools)
Private Sub Handle_Echo(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim text As String = params.GetDefault("text", "")
    Dim result As Map = CreateMap("echo": text)
    ReplyResult(wsClient, id, result)
End Sub

Private Sub Handle_GetProducts(wsClient As WebSocket, id, req As Map)
    Dim list As List = store.GetAllProducts
    ReplyResult(wsClient, id, list)
End Sub

Private Sub Handle_SearchItems(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim q As String = params.GetDefault("query", "")
    Dim list As List = store.Search(q)
    ReplyResult(wsClient, id, list)
End Sub

Private Sub Handle_CreateInvoice(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    ' params expected: items = Array of {product_id, qty}
    Dim items As List = params.GetDefault("items", CreateList())
    If items.Size = 0 Then
        ReplyError(wsClient, id, -32602, "Missing or empty 'items' parameter")
        Return
    End If

    Dim invoice As Map
    invoice.Initialize
    invoice.Put("id", DateTime.Now)
    invoice.Put("created_at", DateTime.Now)
    Dim lines As List
    lines.Initialize
    Dim total As Double = 0
    For Each it As Map In items
        Dim pid = it.Get("product_id")
        Dim qty As Int = it.GetDefault("qty", 1)
        Dim p As Map = store.GetById(pid)
        If p = Null Then Continue
        Dim line As Map = CreateMap("product_id": pid, "name": p.Get("name"), "unit_price": p.Get("price"), "qty": qty, "line_total": p.Get("price") * qty)
        lines.Add(line)
        total = total + line.Get("line_total")
        ' reduce stock
        store.AdjustStock(pid, -qty)
    Next
    invoice.Put("lines", lines)
    invoice.Put("total", total)

    ' For demo: just return invoice (no persistent DB)
    ReplyResult(wsClient, id, invoice)
End Sub

Private Sub Handle_CheckStock(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim pid = params.GetDefault("product_id", Null)
    If pid = Null Then
        ReplyError(wsClient, id, -32602, "Missing 'product_id' parameter")
        Return
    End If
    Dim p As Map = store.GetById(pid)
    If p = Null Then
        ReplyError(wsClient, id, -32602, "Product not found: " & pid)
        Return
    End If
    ReplyResult(wsClient, id, CreateMap("product_id": pid, "stock": p.Get("stock")))
End Sub

Private Sub Handle_GetSalesSummary(wsClient As WebSocket, id, req As Map)
    ' Demo summary computed from store's sales history (if enabled). For now return simple inventory summary.
    Dim summary As Map
    summary.Initialize
    summary.Put("product_count", store.Count)
    summary.Put("total_stock", store.TotalStock)
    ReplyResult(wsClient, id, summary)
End Sub

Private Sub Handle_GetTime(wsClient As WebSocket, id, req As Map)
    ReplyResult(wsClient, id, DateTime.Now)
End Sub

Private Sub Handle_GetServerInfo(wsClient As WebSocket, id, req As Map)
    Dim info As Map = CreateMap("name": "Mini MCP B4J Server", "version": "0.1", "transport": "websocket")
    ReplyResult(wsClient, id, info)
End Sub

' ----------------------
' Utilities for logging (optional)
Sub Log(s As String)
    Dim t As String = DateTime.Now
    Console.WriteLine(t & " | " & s)
End Sub

---
File: ProductStore.bas
' Lightweight in-memory product store for demo purposes.
Sub Class_Globals
    Private products As List ' List of Map
End Sub

Public Sub Initialize
    products.Initialize
    SeedSampleData
End Sub

Private Sub SeedSampleData
    products.Clear
    AddProduct(1, "Apple iPhone 14", 2999.00, 10)
    AddProduct(2, "Samsung Galaxy S23", 2499.00, 15)
    AddProduct(3, "Wireless Mouse", 79.90, 50)
    AddProduct(4, "Mechanical Keyboard", 199.90, 30)
End Sub

Public Sub AddProduct(id As Object, name As String, price As Double, stock As Int)
    Dim m As Map
    m.Initialize
    m.Put("id", id)
    m.Put("name", name)
    m.Put("price", price)
    m.Put("stock", stock)
    products.Add(m)
End Sub

Public Sub GetAllProducts As List
    ' Return a shallow copy
    Dim out As List
    out.Initialize
    For Each p As Map In products
        out.Add(CloneMap(p))
    Next
    Return out
End Sub

Public Sub Search(q As String) As List
    Dim out As List
    out.Initialize
    If q = Null Or q.Trim = "" Then Return out
    q = q.ToLowerCase
    For Each p As Map In products
        If p.Get("name").ToString.ToLowerCase.Contains(q) Then
            out.Add(CloneMap(p))
        End If
    Next
    Return out
End Sub

Public Sub GetById(id As Object) As Map
    For Each p As Map In products
        If p.Get("id") = id Then
            Return CloneMap(p)
        End If
    Next
    Return Null
End Sub

Public Sub AdjustStock(id As Object, delta As Int) As Boolean
    For i = 0 To products.Size - 1
        Dim p As Map = products.Get(i)
        If p.Get("id") = id Then
            Dim newStock As Int = p.Get("stock") + delta
            If newStock < 0 Then newStock = 0
            p.Put("stock", newStock)
            Return True
        End If
    Next
    Return False
End Sub

Public Sub Count As Int
    Return products.Size
End Sub

Public Sub TotalStock As Int
    Dim s As Int = 0
    For Each p As Map In products
        s = s + p.Get("stock")
    Next
    Return s
End Sub

' Helper to clone a Map so we don't leak internal references
Private Sub CloneMap(original As Map) As Map
    Dim m As Map
    m.Initialize
    For Each k As String In original.Keys
        m.Put(k, original.Get(k))
    Next
    Return m
End Sub

---
File: README.txt
Quick start
1) Create a new B4J project.
2) Add three code modules and paste the contents above as files:
   - Main.bas
   - ProductStore.bas
   - README.txt (optional)
3) Ensure you have the WebSocketServer library available (B4J core). The code uses the WebSocketServer type and WebSocket events (Connected, TextMessage, Disconnected).
4) Run the project. The console will show the server address (default ws://0.0.0.0:8888).

Example request (JSON-RPC over WebSocket):
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "get_products",
  "params": {}
}

Example response:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [ {"id":1,"name":"Apple iPhone 14","price":2999.0,"stock":10}, ... ]
}

Notes & next steps
- This demo uses an in-memory store. Replace ProductStore with a SQLite or JDBC-backed implementation for persistence.
- Add authentication, CORS or TLS if exposing to networks.
- Optionally implement MCP "initialize" messages strictly following the MCP spec if needed.

---
---
File: CustomerStore.bas
' Lightweight in-memory customer store
Sub Class_Globals
    Private customers As List ' List of Map
End Sub

Public Sub Initialize
    customers.Initialize
    SeedSampleData
End Sub

Private Sub SeedSampleData
    customers.Clear
    AddCustomer(1, "John Doe", "[email protected]", "012-3456789")
    AddCustomer(2, "Alice Tan", "[email protected]", "013-9876543")
    AddCustomer(3, "Michael Lee", "[email protected]", "017-2223344")
End Sub

Public Sub AddCustomer(id As Object, name As String, email As String, phone As String)
    Dim c As Map
    c.Initialize
    c.Put("id", id)
    c.Put("name", name)
    c.Put("email", email)
    c.Put("phone", phone)
    customers.Add(c)
End Sub

Public Sub GetAllCustomers As List
    Dim out As List
    out.Initialize
    For Each c As Map In customers
        out.Add(CloneMap(c))
    Next
    Return out
End Sub

Public Sub FindById(id As Object) As Map
    For Each c As Map In customers
        If c.Get("id") = id Then Return CloneMap(c)
    Next
    Return Null
End Sub

Public Sub SearchByName(q As String) As List
    Dim out As List
    out.Initialize
    If q = Null Or q.Trim = "" Then Return out
    q = q.ToLowerCase
    For Each c As Map In customers
        If c.Get("name").ToString.ToLowerCase.Contains(q) Then
            out.Add(CloneMap(c))
        End If
    Next
    Return out
End Sub

Private Sub CloneMap(original As Map) As Map
    Dim m As Map
    m.Initialize
    For Each k As String In original.Keys
        m.Put(k, original.Get(k))
    Next
    Return m
End Sub

---
[UPDATE] Main.bas additions below
Please paste these into Main.bas inside appropriate sections.

1) Add to Process_Globals:
    Private cust As CustomerStore

2) In AppStart:
    cust.Initialize

3) Add new handlers:

Private Sub Handle_GetCustomers(wsClient As WebSocket, id, req As Map)
    ReplyResult(wsClient, id, cust.GetAllCustomers)
End Sub

Private Sub Handle_FindCustomer(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim cid = params.GetDefault("customer_id", Null)
    If cid = Null Then
        ReplyError(wsClient, id, -32602, "Missing 'customer_id'")
        Return
    End If
    Dim c As Map = cust.FindById(cid)
    If c = Null Then
        ReplyError(wsClient, id, -32602, "Customer not found: " & cid)
        Return
    End If
    ReplyResult(wsClient, id, c)
End Sub

Private Sub Handle_SearchCustomer(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim q As String = params.GetDefault("query", "")
    ReplyResult(wsClient, id, cust.SearchByName(q))
End Sub

4) Add tool routing inside ws_TextMessage SELECT CASE:

    Case "get_customers"
        Handle_GetCustomers(wsClient, id, req)
    Case "find_customer"
        Handle_FindCustomer(wsClient, id, req)
    Case "search_customer"
        Handle_SearchCustomer(wsClient, id, req)

---
End of project files.

⭐ Real-world example for you​

Imagine you're running POS + Inventory + Customer system.

You connect this MCP server to ChatGPT desktop.

Now ChatGPT can do things like:

Example 1 — Stock Check​

“ChatGPT, which products are low stock today?”
(chatGPT calls your MCP → get_products → provides list)

Example 2 — Customer Lookup​

“Find a customer named Michael and show his info.”
(chatGPT calls → search_customer)

Example 3 — Automatic Sales Report​

“Generate today’s sales report using my database.”
(chatGPT calls → get_sales_summary → formats report)

Example 4 — Auto Invoice​

“Create an invoice for John Doe: 2 iPhones + 1 Keyboard”
(chatGPT calls → find_customer → create_invoice)

✅ Generic MCP Manifest File for B4J

Filename: inventory-mcp.json
File content:

{
"name": "B4J Inventory MCP Server",
"type": "websocket",
"url": "ws://localhost:9999/ws"
}

📌 Where to put this file​

Windows

%LOCALAPPDATA%\ChatGPT\plugins\mcp\inventory-mcp.json

macOS

~/Library/Application Support/ChatGPT/plugins/mcp/inventory-mcp.json

Linux

~/.config/ChatGPT/plugins/mcp/inventory-mcp.json

🚀 What will happen next?​

  1. Start your B4J server on: ws://localhost:9999/ws
  2. Open ChatGPT Desktop.
  3. ChatGPT will automatically detect your server and show a popup:
“ChatGPT found a new MCP tool: B4J Inventory MCP Server — Enable?”
  1. Click Enable.
  2. ChatGPT can now call your MCP tools automatically:
  • get_products
  • search_items
  • get_customers
  • find_customer
  • create_invoice
  • etc.
JSON:
{
  "method": "search_customer",
  "params": { "keyword": "Ali" }
}
 

hatzisn

Expert
Licensed User
Longtime User
Architecture:
B4X:
[B4J Server]
 ├─ REST API (optional)
 ├─ Database (inventory + customer)
 └─ MCP WebSocket Server
       ├─ get_products
       ├─ search_items
       ├─ check_stock
       ├─ get_customer
       ├─ create_invoice
       └─ sales_summary

ChatGPT or any MCP client can:
  • Query inventory
  • Create invoices
  • Update stock
  • Look up customer history
  • Generate reports
  • Perform backend actions
Code:
B4X:
# Project: Mini MCP WebSocket Server (B4J)
# Files included below. Copy each file into a new B4J project with the same filename.

---
File: Main.bas
' Main WebSocket MCP server
Sub Process_Globals
    Private ws As WebSocketServer
    Private ServerPort As Int = 8888
    Private store As ProductStore
End Sub

Sub AppStart (Args() As String)
    Log("Starting Mini MCP WebSocket Server...")
    store.Initialize

    ws.Initialize("ws")
    ws.Start(ServerPort, "0.0.0.0")
    Log($"🚀 MCP WebSocket server started on ws://0.0.0.0:${ServerPort}/")
End Sub

' New client connected
Sub ws_Connected (wsClient As WebSocket)
    Log("Client connected: " & wsClient.GetRemoteAddress)
    ' Send an MCP-style "initialized" capabilities message (not strictly JSON-RPC)
    Dim gen As JSONGenerator
    Dim cap As Map = CreateMap("protocolVersion": "1.0", "capabilities": CreateMap("tools": Array As String("echo","get_products","search_items","create_invoice","check_stock","get_sales_summary","get_time","get_server_info")))
    gen.Initialize(cap)
    wsClient.SendText(gen.ToString)
End Sub

Sub ws_TextMessage (wsClient As WebSocket, msg As String)
    Log("Received: " & msg)
    Dim parser As JSONParser
    parser.Initialize(msg)
    Try
        Dim req As Map = parser.NextObject
        Dim method As String = req.GetDefault("method", "")
        Dim id = req.GetDefault("id", Null)

        Select method
            Case "echo"
                Handle_Echo(wsClient, id, req)
            Case "get_products"
                Handle_GetProducts(wsClient, id, req)
            Case "search_items"
                Handle_SearchItems(wsClient, id, req)
            Case "create_invoice"
                Handle_CreateInvoice(wsClient, id, req)
            Case "check_stock"
                Handle_CheckStock(wsClient, id, req)
            Case "get_sales_summary"
                Handle_GetSalesSummary(wsClient, id, req)
            Case "get_time"
                Handle_GetTime(wsClient, id, req)
            Case "get_server_info"
                Handle_GetServerInfo(wsClient, id, req)
            Case Else
                ReplyError(wsClient, id, -32601, "Method not found: " & method)
        End Select
    Catch
        ReplyError(wsClient, Null, -32700, "Parse error or invalid JSON")
    End Try
End Sub

Sub ws_Disconnected (wsClient As WebSocket)
    Log("Client disconnected: " & wsClient.GetRemoteAddress)
End Sub

' ----------------------
' RPC helper / replies
Private Sub ReplyResult(wsClient As WebSocket, id, result As Object)
    Dim resp As Map = CreateMap("jsonrpc": "2.0", "id": id, "result": result)
    Dim gen As JSONGenerator
    gen.Initialize(resp)
    wsClient.SendText(gen.ToString)
End Sub

Private Sub ReplyError(wsClient As WebSocket, id, code As Int, message As String)
    Dim resp As Map = CreateMap("jsonrpc": "2.0")
    If id <> Null Then resp.Put("id", id)
    resp.Put("error", CreateMap("code": code, "message": message))
    Dim gen As JSONGenerator
    gen.Initialize(resp)
    wsClient.SendText(gen.ToString)
End Sub

' ----------------------
' Handlers (business / database tools)
Private Sub Handle_Echo(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim text As String = params.GetDefault("text", "")
    Dim result As Map = CreateMap("echo": text)
    ReplyResult(wsClient, id, result)
End Sub

Private Sub Handle_GetProducts(wsClient As WebSocket, id, req As Map)
    Dim list As List = store.GetAllProducts
    ReplyResult(wsClient, id, list)
End Sub

Private Sub Handle_SearchItems(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim q As String = params.GetDefault("query", "")
    Dim list As List = store.Search(q)
    ReplyResult(wsClient, id, list)
End Sub

Private Sub Handle_CreateInvoice(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    ' params expected: items = Array of {product_id, qty}
    Dim items As List = params.GetDefault("items", CreateList())
    If items.Size = 0 Then
        ReplyError(wsClient, id, -32602, "Missing or empty 'items' parameter")
        Return
    End If

    Dim invoice As Map
    invoice.Initialize
    invoice.Put("id", DateTime.Now)
    invoice.Put("created_at", DateTime.Now)
    Dim lines As List
    lines.Initialize
    Dim total As Double = 0
    For Each it As Map In items
        Dim pid = it.Get("product_id")
        Dim qty As Int = it.GetDefault("qty", 1)
        Dim p As Map = store.GetById(pid)
        If p = Null Then Continue
        Dim line As Map = CreateMap("product_id": pid, "name": p.Get("name"), "unit_price": p.Get("price"), "qty": qty, "line_total": p.Get("price") * qty)
        lines.Add(line)
        total = total + line.Get("line_total")
        ' reduce stock
        store.AdjustStock(pid, -qty)
    Next
    invoice.Put("lines", lines)
    invoice.Put("total", total)

    ' For demo: just return invoice (no persistent DB)
    ReplyResult(wsClient, id, invoice)
End Sub

Private Sub Handle_CheckStock(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim pid = params.GetDefault("product_id", Null)
    If pid = Null Then
        ReplyError(wsClient, id, -32602, "Missing 'product_id' parameter")
        Return
    End If
    Dim p As Map = store.GetById(pid)
    If p = Null Then
        ReplyError(wsClient, id, -32602, "Product not found: " & pid)
        Return
    End If
    ReplyResult(wsClient, id, CreateMap("product_id": pid, "stock": p.Get("stock")))
End Sub

Private Sub Handle_GetSalesSummary(wsClient As WebSocket, id, req As Map)
    ' Demo summary computed from store's sales history (if enabled). For now return simple inventory summary.
    Dim summary As Map
    summary.Initialize
    summary.Put("product_count", store.Count)
    summary.Put("total_stock", store.TotalStock)
    ReplyResult(wsClient, id, summary)
End Sub

Private Sub Handle_GetTime(wsClient As WebSocket, id, req As Map)
    ReplyResult(wsClient, id, DateTime.Now)
End Sub

Private Sub Handle_GetServerInfo(wsClient As WebSocket, id, req As Map)
    Dim info As Map = CreateMap("name": "Mini MCP B4J Server", "version": "0.1", "transport": "websocket")
    ReplyResult(wsClient, id, info)
End Sub

' ----------------------
' Utilities for logging (optional)
Sub Log(s As String)
    Dim t As String = DateTime.Now
    Console.WriteLine(t & " | " & s)
End Sub

---
File: ProductStore.bas
' Lightweight in-memory product store for demo purposes.
Sub Class_Globals
    Private products As List ' List of Map
End Sub

Public Sub Initialize
    products.Initialize
    SeedSampleData
End Sub

Private Sub SeedSampleData
    products.Clear
    AddProduct(1, "Apple iPhone 14", 2999.00, 10)
    AddProduct(2, "Samsung Galaxy S23", 2499.00, 15)
    AddProduct(3, "Wireless Mouse", 79.90, 50)
    AddProduct(4, "Mechanical Keyboard", 199.90, 30)
End Sub

Public Sub AddProduct(id As Object, name As String, price As Double, stock As Int)
    Dim m As Map
    m.Initialize
    m.Put("id", id)
    m.Put("name", name)
    m.Put("price", price)
    m.Put("stock", stock)
    products.Add(m)
End Sub

Public Sub GetAllProducts As List
    ' Return a shallow copy
    Dim out As List
    out.Initialize
    For Each p As Map In products
        out.Add(CloneMap(p))
    Next
    Return out
End Sub

Public Sub Search(q As String) As List
    Dim out As List
    out.Initialize
    If q = Null Or q.Trim = "" Then Return out
    q = q.ToLowerCase
    For Each p As Map In products
        If p.Get("name").ToString.ToLowerCase.Contains(q) Then
            out.Add(CloneMap(p))
        End If
    Next
    Return out
End Sub

Public Sub GetById(id As Object) As Map
    For Each p As Map In products
        If p.Get("id") = id Then
            Return CloneMap(p)
        End If
    Next
    Return Null
End Sub

Public Sub AdjustStock(id As Object, delta As Int) As Boolean
    For i = 0 To products.Size - 1
        Dim p As Map = products.Get(i)
        If p.Get("id") = id Then
            Dim newStock As Int = p.Get("stock") + delta
            If newStock < 0 Then newStock = 0
            p.Put("stock", newStock)
            Return True
        End If
    Next
    Return False
End Sub

Public Sub Count As Int
    Return products.Size
End Sub

Public Sub TotalStock As Int
    Dim s As Int = 0
    For Each p As Map In products
        s = s + p.Get("stock")
    Next
    Return s
End Sub

' Helper to clone a Map so we don't leak internal references
Private Sub CloneMap(original As Map) As Map
    Dim m As Map
    m.Initialize
    For Each k As String In original.Keys
        m.Put(k, original.Get(k))
    Next
    Return m
End Sub

---
File: README.txt
Quick start
1) Create a new B4J project.
2) Add three code modules and paste the contents above as files:
   - Main.bas
   - ProductStore.bas
   - README.txt (optional)
3) Ensure you have the WebSocketServer library available (B4J core). The code uses the WebSocketServer type and WebSocket events (Connected, TextMessage, Disconnected).
4) Run the project. The console will show the server address (default ws://0.0.0.0:8888).

Example request (JSON-RPC over WebSocket):
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "get_products",
  "params": {}
}

Example response:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [ {"id":1,"name":"Apple iPhone 14","price":2999.0,"stock":10}, ... ]
}

Notes & next steps
- This demo uses an in-memory store. Replace ProductStore with a SQLite or JDBC-backed implementation for persistence.
- Add authentication, CORS or TLS if exposing to networks.
- Optionally implement MCP "initialize" messages strictly following the MCP spec if needed.

---
---
File: CustomerStore.bas
' Lightweight in-memory customer store
Sub Class_Globals
    Private customers As List ' List of Map
End Sub

Public Sub Initialize
    customers.Initialize
    SeedSampleData
End Sub

Private Sub SeedSampleData
    customers.Clear
    AddCustomer(1, "John Doe", "[email protected]", "012-3456789")
    AddCustomer(2, "Alice Tan", "[email protected]", "013-9876543")
    AddCustomer(3, "Michael Lee", "[email protected]", "017-2223344")
End Sub

Public Sub AddCustomer(id As Object, name As String, email As String, phone As String)
    Dim c As Map
    c.Initialize
    c.Put("id", id)
    c.Put("name", name)
    c.Put("email", email)
    c.Put("phone", phone)
    customers.Add(c)
End Sub

Public Sub GetAllCustomers As List
    Dim out As List
    out.Initialize
    For Each c As Map In customers
        out.Add(CloneMap(c))
    Next
    Return out
End Sub

Public Sub FindById(id As Object) As Map
    For Each c As Map In customers
        If c.Get("id") = id Then Return CloneMap(c)
    Next
    Return Null
End Sub

Public Sub SearchByName(q As String) As List
    Dim out As List
    out.Initialize
    If q = Null Or q.Trim = "" Then Return out
    q = q.ToLowerCase
    For Each c As Map In customers
        If c.Get("name").ToString.ToLowerCase.Contains(q) Then
            out.Add(CloneMap(c))
        End If
    Next
    Return out
End Sub

Private Sub CloneMap(original As Map) As Map
    Dim m As Map
    m.Initialize
    For Each k As String In original.Keys
        m.Put(k, original.Get(k))
    Next
    Return m
End Sub

---
[UPDATE] Main.bas additions below
Please paste these into Main.bas inside appropriate sections.

1) Add to Process_Globals:
    Private cust As CustomerStore

2) In AppStart:
    cust.Initialize

3) Add new handlers:

Private Sub Handle_GetCustomers(wsClient As WebSocket, id, req As Map)
    ReplyResult(wsClient, id, cust.GetAllCustomers)
End Sub

Private Sub Handle_FindCustomer(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim cid = params.GetDefault("customer_id", Null)
    If cid = Null Then
        ReplyError(wsClient, id, -32602, "Missing 'customer_id'")
        Return
    End If
    Dim c As Map = cust.FindById(cid)
    If c = Null Then
        ReplyError(wsClient, id, -32602, "Customer not found: " & cid)
        Return
    End If
    ReplyResult(wsClient, id, c)
End Sub

Private Sub Handle_SearchCustomer(wsClient As WebSocket, id, req As Map)
    Dim params As Map = req.GetDefault("params", CreateMap())
    Dim q As String = params.GetDefault("query", "")
    ReplyResult(wsClient, id, cust.SearchByName(q))
End Sub

4) Add tool routing inside ws_TextMessage SELECT CASE:

    Case "get_customers"
        Handle_GetCustomers(wsClient, id, req)
    Case "find_customer"
        Handle_FindCustomer(wsClient, id, req)
    Case "search_customer"
        Handle_SearchCustomer(wsClient, id, req)

---
End of project files.

⭐ Real-world example for you​

Imagine you're running POS + Inventory + Customer system.

You connect this MCP server to ChatGPT desktop.

Now ChatGPT can do things like:

Example 1 — Stock Check​


(chatGPT calls your MCP → get_products → provides list)

Example 2 — Customer Lookup​


(chatGPT calls → search_customer)

Example 3 — Automatic Sales Report​


(chatGPT calls → get_sales_summary → formats report)

Example 4 — Auto Invoice​


(chatGPT calls → find_customer → create_invoice)

✅ Generic MCP Manifest File for B4J

Filename: inventory-mcp.json
File content:

{
"name": "B4J Inventory MCP Server",
"type": "websocket",
"url": "ws://localhost:9999/ws"
}

📌 Where to put this file​

Windows

%LOCALAPPDATA%\ChatGPT\plugins\mcp\inventory-mcp.json

macOS

~/Library/Application Support/ChatGPT/plugins/mcp/inventory-mcp.json

Linux

~/.config/ChatGPT/plugins/mcp/inventory-mcp.json

🚀 What will happen next?​

  1. Start your B4J server on: ws://localhost:9999/ws
  2. Open ChatGPT Desktop.
  3. ChatGPT will automatically detect your server and show a popup:

  1. Click Enable.
  2. ChatGPT can now call your MCP tools automatically:
  • get_products
  • search_items
  • get_customers
  • find_customer
  • create_invoice
  • etc.
JSON:
{
  "method": "search_customer",
  "params": { "keyword": "Ali" }
}

Sounds easy the way it is described. I was disappointed when I was searching for the same thing before some time. The jsonrpc protocol it describes has completely disappointed me because I believed that it has to be implemented in full. F.e.

Request:

B4X:
{
    "jsonrpc": "2.0",
    "method": "get_current_weather",
    "params": {
        "city": "Piraeus",
        "unit": "celsius"
    },
    "id": 167825345
}

Response success (so far so good):

B4X:
{
    "jsonrpc": "2.0",
    "result": {
        "temperature": 18,
        "unit": "celsius",
        "description": "Clear skies"
    },
    "id": 167825345
}

Response Error (here we have a problem - error codes {f.e. -32601} are defined strictly and have to be implemented fully or at least that is what I thought):

B4X:
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32601,
        "message": "Method not found: get_current_weather"
    },
    "id": 167825345
}


Code RangesDescription
-32768 to -32000Pre-defined Errors
-32000 to -32099Server Error
Other IntegersApplication Error
 
Last edited:

hatzisn

Expert
Licensed User
Longtime User
Sounds easy the way it is described. I was disappointed when I was searching for the same thing before some time. The jsonrpc protocol it describes has completely disappointed me because I believed that it has to be implemented in full. F.e.

Request:

B4X:
{
    "jsonrpc": "2.0",
    "method": "get_current_weather",
    "params": {
        "city": "Piraeus",
        "unit": "celsius"
    },
    "id": 167825345
}

Response success (so far so good):

B4X:
{
    "jsonrpc": "2.0",
    "result": {
        "temperature": 18,
        "unit": "celsius",
        "description": "Clear skies"
    },
    "id": 167825345
}

Response Error (here we have a problem - error codes {f.e. -32601} are defined strictly and have to be implemented fully or at least that is what I thought):

B4X:
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32601,
        "message": "Method not found: get_current_weather"
    },
    "id": 167825345
}


Code RangesDescription
-32768 to -32000Pre-defined Errors
-32000 to -32099Server Error
Other IntegersApplication Error

Here are the definitions of the error object:

 

hatzisn

Expert
Licensed User
Longtime User
The link contained in the link of my previous post was broken so I found the broken link in the way back machine. Here it is:

Specification for Fault Code Interoperability, version 20010516​

Author: Dan Libby

Major Contributors:
- Charles Cook
- Dave Winer
- Eric Kidd
- John Wilson
- S. Alexander Jacobson

ChangeLog​

1.0 (draft 1)Initial draft release
1.0 (draft 2)Adding reserved range for implementation specific errors and differentiating between unknown encoding and incorrect char for encoding
20010516changed system.interopFaultCodes to system.getCapabilities.
changed version to date based integer which is useful for comparisons

Charter​

As the number of xml-rpc server implementations has proliferated, so has the number of error codes and descriptions. The charter of this specification is to define a small set of error codes that are common across most server implementations so that clients can programmatically handle common errors such as "method not found" or "parse error".

Goals​

  • improved interoperability
  • ease of implementation
  • maintain xml-rpc spec compatibility
  • maintain backwards compatibility with existing xmlrpc implementations and apps
  • extensibility. both for this spec and for applications & implementations conforming to it

Decisions​

  • K.I.S.S. not trying to solve all the world's problems.
  • namespaces are nice, but add complexity. this is a simple problem
  • define general error classes only. specifics may be filled in via description

Reserved Error Codes​

This spec reserves the numbers -32768 .. -32000, inclusive, for use as pre-defined xmlrpc error codes. Any implementation which conforms to this spec should not allow the application to set an error code within this range. Any error codes within this range not defined explicitly below are reserved for future use. Any implementation which uses them is in non-compliance.

Defined Error Codes​

-32700 ---> parse error. not well formed
-32701 ---> parse error. unsupported encoding
-32702 ---> parse error. invalid character for encoding
-32600 ---> server error. invalid xml-rpc. not conforming to spec.
-32601 ---> server error. requested method not found
-32602 ---> server error. invalid method parameters
-32603 ---> server error. internal xml-rpc error
-32500 ---> application error
-32400 ---> system error
-32300 ---> transport errorIn addition, the range
-32099 .. -32000, inclusive is reserved for implementation defined server errors. Server errors which do not cleanly map to a specific error defined by this spec should be assigned to a number in this range. This leaves the remainder of the space available for application defined errors.

system.getCapabilities​

system.getCapabilities is a standard method for determining if a given capability is supported by a given server. For our purposes, a capability may be defined as a layer on top of xml-rpc for which a spec exists. Thus, support for standard faults is a capability.

Any server which conforms to this spec must implement system.getCapabilities and return a member struct representing the faults capability. The struct shall be identified by the key "faults_interop". The specUrl should be "http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php", and the specVersion should be the integer 20010516. Here is an example response in pseudo-code:

struct { faults_interop =&gt; struct { specUrl =&gt; "http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php", specVersion =&gt; 20010516 } }
A fault or any other response indicates that the server is not in conformance with this spec, and fault error codes should be treated as application defined error codes.
 
Last edited:

aeric

Expert
Licensed User
Longtime User
It turns out ChatGPT desktop using free account has no MCP Connector feature. I will check for alternatives like Claude.
 

aeric

Expert
Licensed User
Longtime User
How if we can integrate a web UI or server handler connect to Gemini API inside the MCP server as a client?
 

hatzisn

Expert
Licensed User
Longtime User
How if we can integrate a web UI or server handler connect to Gemini API inside the MCP server as a client?

I am not sure I have understood the question. You mean if we use a web UI for the LLM or a server handler that incorporates the LLM how to connect to the MCP Server through the LLM as a client of the server handler or the Web UI? All the videos I have seen connect the LLM directly to the LLM client and use the MCP server through the LLM. But then you could easily ask Gemini for it. I did it and it makes obvious that you use the tools section of the JSON request to the LLM and if it wants to use a tool it returns it to you along with the parameters and then by your backend or the Web UI you use jsonrpc 2.0 to connect to the mcp server.
 
Last edited:

aeric

Expert
Licensed User
Longtime User
I am not sure I have understood the question. You mean if we use a web UI for the LLM or a server handler that incorporates the LLM how to connect to the MCP Server through the LLM as a client of the server handler or the Web UI? All the videos I have seen connect the LLM directly to the LLM client and use the MCP server through the LLM. But then you could easily ask Gemini for it. I did it and it makes obvious that you use the tools section of the JSON request and if it wants to use a tool it returns it to you along with the parameters and then by your backend or the Web UI you use jsonrpc 2.0 to connect to the mcp server.
Question is which AI client do you use to connect to the MCP Server?
 

Magma

Expert
Licensed User
Longtime User
...Hi there... guys... you can use any AI client, ofcourse the results may be different!

also check the TOON (vs JSON, yaml) - tokens using are extremely low... (is something like csv... so simple)


until today, i ve used OpenAI, Perplexity Models, gemini... I think openAI is extremely good, but i think claudie is for us... (developers... understand us very well)
 

hatzisn

Expert
Licensed User
Longtime User
I am only aware of Gradio for python as an AI Client and Web UI for ollama. Though, given that now we know how to do it, we could easily implement a client even without a front end (just by using a server handler).
 

hatzisn

Expert
Licensed User
Longtime User
I am only aware of Gradio for python as an AI Client and Web UI for ollama. Though, given that now we know how to do it, we could easily implement a client even without a front end (just by using a server handler).

It could be implemented in the same app also without the need for a seperate MCP Server.

 

aeric

Expert
Licensed User
Longtime User
If only we can use an existing AI client that can discover the MCP server.
I am thinking at the end user perspective and what I explained in first post. To provide value to the end users business.
 

aeric

Expert
Licensed User
Longtime User
...Hi there... guys... you can use any AI client, ofcourse the results may be different!

also check the TOON (vs JSON, yaml) - tokens using are extremely low... (is something like csv... so simple)


until today, i ve used OpenAI, Perplexity Models, gemini... I think openAI is extremely good, but i think claudie is for us... (developers... understand us very well)
Is there any AI client can discover TOON format?
 

Magma

Expert
Licensed User
Longtime User
Is there any AI client can discover TOON format?
Hmmm I am just loose the interesting discussion... you ve mean AI Client for MCP.

I was thinking assistant creation via API... you can set the guides take TOON and give TOON.. and not getting out of it...
 
Top