IBM watsonx.data remote Model Context Protocol (MCP) server

IBM watsonx.data remote MCP server is a Model Context Protocol (MCP) compliant service that seamlessly connects AI agents with document libraries in watsonx.data, enabling intelligent data retrieval and interaction. While you must manually set up and configure a local MCP server, a remote MCP server is hosted on IBM infrastructure and accessed over a network, making it easier to collaborate and reducing the need for manual setup and configuration.

Key features of watsonx.data remote MCP server:

  • Dynamic discovery and registration: Automatically detects and registers document libraries as MCP tools.
  • Natural language interface: Query document libraries using natural language and receive human-readable responses.
  • Setup and Configuration: No setup or configuration is required, as the solution is already deployed on IBM infrastructure
  • Framework agnostic integration: Plug directly into the preferred agentic frameworks with native MCP compatibility.
Note: To uniquely identify each document library, the MCP server adds a tool and the tool name combines the document library name and document library id. For example: invoice_document_library77e4b4dd_479e_4406_acc4_ce154c96266c.

Prerequisites

Before you using the remote MCP server, make sure that the following prerequisites are met:

  • You must have valid access credentials (bearer token) and certificate.
  • Make sure that your AI agent framework supports the MCP protocol.

Remote MCP server functional overview

The remote MCP server serves as a bridge between AI agents and document libraries within the watsonx.data platform. It enables seamless discovery, interaction, and intelligent querying of document libraries using the Model Context Protocol (MCP).

  • Document Library Registration: Automatically detects document libraries in your watsonx.data instance and registers each as an MCP tool.
  • Agent Interaction: AI agents establish a connection with the MCP server using the MCP protocol. After the connection is established, they can discover and all registered document libraries.
  • Query Processing: The MCP server receives natural language queries from agents, translates them into appropriate API calls to the relevant document library, and returns the results in a human-readable format.
  • Metadata Handling: The MCP server provides custom metadata from document libraries to connected agents to enhance performance. If metadata is unavailable, the server uses fallback logic to retrieve it automatically.

Connect to the remote MCP Server

To connect to the remote MCP server, use the following endpoint:

https://<your-instance-url>/api/v1/mcp/

Replace <your-instance-url> with the URL of your specific watsonx.data instance. Agents use this endpoint to discover and interact with available document libraries.

Using custom metadata

The remote MCP server can pass custom metadata to connected AI agents. Custom metadata fields such as document_library_id, container_id, and container_type are defined as part of the tool configuration and are made available to agents when they call the list tools method.

Agents can access metadata by:

  • Calling the list tools method.

  • Iterating through each tool object.

  • Reading metadata fields:

    • x_document_library_id - document_library_id
    • x_container_id - container_id
    • x_container_type - container_type

Agents can also invoke tools without providing custom metadata. In such cases, the MCP server retrieves the necessary metadata, which is essential for accessing the retrieval service.

To fetch metadata from a tool object, use standard Python getattr calls. For example, you can fetch the document library ID, container ID, and container type using:

   x_doc_lib_id = getattr(tool, "x_document_library_id", None)
   x_container_id = getattr(tool, "x_container_id", None)
   x_container_type = getattr(tool, "x_container_type", None)

After metadata becomes available, the agent invokes a tool by passing the metadata along with the arguments parameter to the call tool method of the remote MCP server. The agent includes the metadata as a new key named metadata in the arguments dictionary. The MCP server extracts the metadata and uses it to route the request to the appropriate document library tool. The following exmaple demonstrates the call tool method.

   arguments['metadata'] = metadata_value
   # Create the tool call request payload
   request_payload = {
   "jsonrpc": "2.0",
   "method": "tools/call",
   "params": {"name": tool_name, "arguments": arguments},
   "id": f"call-{tool_name}",
   }
   async with httpx.AsyncClient(timeout=30.0) as client:
   response = await client.post(
   self.server_url,
   json=request_payload,

   headers={
   "Content-Type": "application/json",
   "Accept": "application/json, text/event-stream",
   "authorization": auth_token,
   "Mcp-Session-Id": sessionId,
   },
   )

Integrating with watsonx Orchestrate

Complete the following steps to integrate the remote MCP server with watsonx Orchestrate:

  1. Install IBM watsonx Orchestrate Agent Development Kit (ADK) and complete the initial setup. For more information, refer Welcome to IBM watsonx Orchestrate Agent Development Kit.

  2. Run the following command to create connections.

    orchestrate connections add -a <CONN_NAME>
    orchestrate connections configure --app-id <CONN_NAME> --environment draft -t team -k bearer
    orchestrate connections set-credentials --app-id <CONN_NAME> --env draft --token <TOKEN>
    
  3. Run the following command to import toolkit to watsonx Orchestrate. For more information on toolkits, refer Managing Toolkits.

    orchestrate toolkits import \
    --kind mcp \
    --name <TOOLKIT_NAME> \
    --description "remote MCP server" \
    --url "https://<URL>/api/v1/mcp/"  \
    --transport "streamable_http" \
    --tools "*" \
    --app-id " <CONN_NAME> "
    

    The following example demonstrates how to add tools as a toolkit in watsonx Orchestrate.

    orchestrate toolkits import \
    --kind mcp \
    --name remote_mcp \
    --description "test remote mcp" \
    --url "https://cpd-cpd-instance.apps.perf1.ibm.prestodb.dev/api/v1/mcp/" \
    --transport "streamable_http" \
    --tools "*" \
    --app-id " test_remote "
    

Integrating with other agentic framework

Complete the following steps to integrate the remote MCP server with LangChain and enable AI agents to invoke tools using custom metadata:

Note: All steps below are intended to be executed within a Python environment, specifically within a Jupyter notebook.
  1. Run the following code to install dependencies.

    !pip3 install mcp langgraph langchain_mcp_adapters langchain_ibm langchain
    
  2. Run the following code to add environment variables.

    os.environ["WATSONX_API_KEY"]="<>"
    os.environ["WATSONX_PROJECT_ID"]="<>"
    os.environ["WATSONX_URL"]="<>"
    os.environ["MCP_ENDPOINT"]="<>"
    
    
    Note:
    • To obtain your WATSONX_API_KEY, refer API keys.
    • To find your WATSONX_PROJECT_ID, navigate to your project in the watsonx.ai web console, click the Manage tab, and locate the Project ID under the General Information section.
    • For WATSONX_URL, refer Introduction to IBM watsonx.ai Runtime and choose the correct instance URL based on your region.

  3. Run the following code to imprort dependencies.

    from mcp import ClientSession
    from mcp.client.streamable_http import streamablehttp_client
    import os
    from langgraph.prebuilt import create_react_agent
    
  4. Run the following code to load tools from the remote MCP server.

    authToken = "<>"
    headers = {"authorization" : authToken}
    mcp_endpoint = os.getenv("MCP_ENDPOINT")
    async with streamablehttp_client(url = mcp_endpoint, headers=headers) as (read, write, _):
          async with ClientSession(read, write) as session:
                # Initialize the connection
                await session.initialize()
                # Get tools
                tool_response = await session.list_tools()
                tools = tool_response.tools
                print(tools)
                print(len(tools))
    
    Note:

    To generate authToken, refer Authentication.

  5. Run the following code to extract custom metadata from tools.

    from typing import Any
    metadata : dict[str, dict[str,Any]] = {}
    for tool in tools:
       x_doc_lib_id = getattr(tool, "x_document_library_id", None)
       x_container_id = getattr(tool, "x_container_id", None)
       x_container_type = getattr(tool, "x_container_type", None)
       metadata[tool.name] = {'document_library_id':x_doc_lib_id,
             'container_id' : x_container_id,'container_type':x_container_type}
       print(f"{tool.name} => {metadata[tool.name]}")
    
  6. Run the following code to set up ChatWatsonx llm.

    from langchain_ibm import ChatWatsonx
    llm = ChatWatsonx(
    model_id="meta-llama/llama-3-3-70b-instruct",
    url = os.getenv("WATSONX_URL"),
    apikey = os.getenv("WATSONX_API_KEY"),
    project_id = os.getenv("WATSONX_PROJECT_ID"),
    temperature=0,
    )
    
  7. Run the following code to define custom tool invocation function.

    import httpx
    import json
    
    async def call_tool_with_metadata(tool_name: str, arguments: dict[str, Any],sessionId):
          response = ""
          metadata_value = metadata[tool_name]
          arguments['metadata'] = metadata_value
          if metadata_value:
                print(f"{tool_name} Metadata available: {metadata_value}")
          else:
                print("No metadata to send")
          if metadata_value:
                # Create the tool call request payload
                request_payload = {
                "jsonrpc": "2.0",
                "method": "tools/call",
                "params": {"name": tool_name, "arguments": arguments},
                "id": f"call-{tool_name}",
                }
                try:
                        # Use httpx directly to send the request with custom headers
                        async with httpx.AsyncClient(timeout=120.0) as client:
                              response = await client.post(
                              url = mcp_endpoint,
                              json=request_payload,
    
                              headers={
                              "Content-Type": "application/json",
                              "Accept": "application/json, text/event-stream",
                              "authorization": authToken,
                              "Mcp-Session-Id": sessionId,
                              },
                              )
    
                              if response.status_code == 200:
                                     result = response.text
                                     print(f"Success 📥 Response: {json.dumps(result, indent=2)}\n")
                                     print("Generating response .... \n")
                                     return json.dumps(result, indent=2)
                              else:
                                     print(f" ❌ Error: HTTP {response.status_code}")
                                     print(f" Response: {response.text}")
                                     return response.text
                except Exception as e:
                      print(f" ❌ Request failed: {e}")
                finally:
                      if response.status_code != 200:
                             print(response)
          else:
                # Fallback to standard call_tool (won't include custom headers)
                print(" ⚠️ Using standard call_tool (no custom headers)")
                result = await session.call_tool(tool_name, arguments)
                print(f" 📥 Response: {result}")
                return result
    
  8. Run the following code to convert MCP tools to langchain tools.

    from langchain.tools import StructuredTool
    def convert_mcp_tool_to_langchain_tool(mcp_tool, call_tool_fn, sessionId):
    """Convert MCP Tool -> LangChain StructuredTool with real execution logic."""
    
         async def tool_func(query: str) -> str:
               arguments = {"query": query}
               # delegate the execution to your function
               result = await call_tool_fn(mcp_tool.name, arguments, sessionId)
               return result
    
         return StructuredTool.from_function(
                func=tool_func,
                name=mcp_tool.name,
                description=mcp_tool.description or "No description provided",
                metadata={
                "x_document_library_id": mcp_tool.x_document_library_id,
                "x_container_id": mcp_tool.x_container_id,
                "x_container_type": mcp_tool.x_container_type,
                },
                coroutine=tool_func
         )
    
  9. Run the following code to invoke the agent.

    from langchain.schema import AIMessage
    async with streamablehttp_client(url = mcp_endpoint, headers=headers) as (read, write, get_session_id):
          async with ClientSession(read, write) as session:
                # Initialize the connection
                await session.initialize()
                session_id = get_session_id()
                print(session_id)
    
                # Get tools
                tool_response = await session.list_tools()
                tools = tool_response.tools
                langchain_tools = [
                convert_mcp_tool_to_langchain_tool(
                t,
                call_tool_with_metadata,
                session_id
                )
                for t in tool_response.tools
                ]
                tools_dict = {}
                print(f"Below are the available {len(langchain_tools)} tools \n ")
                for index,tool in enumerate(langchain_tools):
                       print(f"{index+1} {tool.name}\n")
                       tools_dict[tool.name.lower()] = tool
                while True:
                      target_tool = input("Enter the name of tool you want to 	query or type 'q' to quit \n")
                      target_tool = target_tool.strip().rstrip(".").lower()
                      if target_tool == 'q':
                             break
                      if tools_dict.get(target_tool) == None:
                             print(f"Tool {target_tool} not available \n Please choose one from the available tools \n")
                             continue
                      print(f"Selected tool : {tools_dict.get(target_tool)}\n")
                      agent = create_react_agent(
                      model=llm,
                      tools=[tools_dict.get(target_tool)],
                      )
                      query = input("Enter the Query \n")
                      result = await agent.ainvoke({"messages": [{"role": "user", 	"content": query}]})
                      ai_messages = [m for m in result["messages"] if isinstance(m, AIMessage)]
                      final_answer = ai_messages[-1].content
                      if final_answer:
                              print(f"{final_answer}\n")
                      else:
                              print(f"{result}\n")
    

Learn more

Parent topic: Retrieval service