Skip to main content

Server Introduction

In a typical RAG system, the overall workflow usually consists of multiple functional modules, such as the Retriever and the Generator. Each module is responsible for a specific task, and they work together through pipeline orchestration to complete complex question-answering and reasoning processes. In UR-2.0, we adopt the MCP (Model Context Protocol) architecture to standardize and encapsulate these functional modules into a unified implementation—Server.
A Server is essentially an independent RAG module component with a specific function.
Each Server encapsulates a core task logic (such as retrieval, generation, or evaluation) and exposes standardized interfaces to the outside world via function-level Tools. With this mechanism, Servers can be flexibly combined, invoked, and reused within a complete Pipeline, enabling modular and extensible system construction.

Server Development

To help you better understand how to use Servers, this section demonstrates a complete workflow for developing a custom Server from scratch.

Step 1: Create the Server File

First, create a new folder named sayhello under the servers directory, and then create a source folder sayhello/src inside it. Next, create a file named sayhello.py in the src directory—this will serve as the main entry point for the Server. In UR-2.0, all Servers are instantiated via the UltraRAG_MCP_Server base class. Example:
servers/sayhello/src/sayhello.py
from ultrarag.server import UltraRAG_MCP_Server

app = UltraRAG_MCP_Server("sayhello")

if __name__ == "__main__":
    # Start the sayhello server using stdio transport
    app.run(transport="stdio")

Step 2: Implement Tool Functions

Use the @app.tool decorator to register tool functions (Tools). These functions are invoked during Pipeline execution to perform specific logic. For example, the following code defines a simple greeting function greet, which takes a name as input and returns a greeting message:
servers/sayhello/src/sayhello.py
from typing import Dict
from ultrarag.server import UltraRAG_MCP_Server

app = UltraRAG_MCP_Server("sayhello")

@app.tool(output="name->msg")
def greet(name: str) -> Dict[str, str]:
    ret = f"Hello, {name}!"
    app.logger.info(ret)
    return {"msg": ret}

if __name__ == "__main__":
    # Start the sayhello server using stdio transport
    app.run(transport="stdio")

Step 3: Configure Parameter File

Next, create a parameter configuration file named parameter.yaml under the sayhello folder.
This file declares the input parameters required by each Tool and their default values, allowing automatic loading and passing during Pipeline execution.
Example:
https://mintcdn.com/ultrarag/T7GffHzZitf6TThi/images/yaml.svg?fit=max&auto=format&n=T7GffHzZitf6TThi&q=85&s=69b41e79144bc908039c2ee3abbb1c3bservers/sayhello/parameter.yaml
name: UltraRAG 2.0
Here, we define the parameter name with the default value "UltraRAG 2.0".

Parameter Registration Mechanism

If different Prompt Tools have conflicting parameter names, refer to the “Multiple Prompt Tool Invocation” section in the Prompt Server documentation for resolution.
During the build phase, UR-2.0 automatically reads each Server’s parameter.yaml file to detect and register the parameters required by its Tool functions.
When using this mechanism, keep the following points in mind:
  • Parameter Sharing: When multiple Tools need to share a parameter (e.g., template, model_name_or_path), you only need to declare it once in parameter.yaml, and it can be reused across Tools.
  • Field Overriding Risks: If multiple Tools use the same parameter name but with different meanings or default values, you should explicitly distinguish them by using different field names to prevent unintentional overwriting in the automatically generated configuration file.
  • Automatic Context Inference: If a Tool’s input parameter is not defined in parameter.yaml, UR-2.0 will attempt to infer its value from the runtime context (i.e., from the output of upstream Tools). Therefore, only explicitly define parameters in parameter.yaml when they cannot be automatically inferred.

Encapsulating Shared Variables with Classes

In some scenarios, you may want to maintain shared states or variables within the same Server—for example, model instances, cache objects, or configuration settings.
In such cases, you can encapsulate the Server as a class and define shared variables and Tool registrations during initialization.
The following example shows how to encapsulate the sayhello Server as a class to share internal variables:
servers/sayhello/src/sayhello.py
from typing import Dict
from ultrarag.server import UltraRAG_MCP_Server

app = UltraRAG_MCP_Server("sayhello")

class Sayhello:
    def __init__(self, mcp_inst: UltraRAG_MCP_Server):
        mcp_inst.tool(self.greet, output="name->msg")
        self.sen = "Nice to meet you"

    def greet(self, name: str) -> Dict[str, str]:
        ret = f"Hello, {name}! {self.sen}!"
        app.logger.info(ret)
        return {"msg": ret}

if __name__ == "__main__":
    Sayhello(app)
    app.run(transport="stdio")
In this example, self.sen simulates a shared variable that can be accessed by different Tools within the same Server.
This approach is especially useful when loading models or maintaining configurations that are reused across multiple functions.