Implement an MCP Server with ORDS and MLE JavaScript

Oracle provides MCP Server options for your database

If you are serious about building out a secure, scalable and fully integrated MCP experience in your organisation then focus on dedicated Oracle products rather than rolling your own. Start with:

If you want to explore the specification and learn some concepts then read on…

  1. 🎯 The Objective
  2. 📍 Introduction
  3. 🔍 What Is MCP?
  4. 🚀 Why ORDS + MLE JavaScript?
  5. 🛠️ MCP Handler Implementation in ORDS
    1. 🧩 1. Setup – REST enable the database schema and grant MLE privileges
    2. 🧱 2. Defining the MCP Module and Template
    3. ✍️ 3. JavaScript MLE Handler Source
  6. 🔐 Securing the MCP Handler
  7. 🧪 Deploying and Testing the MCP Server
    1. 🛰️ Deploy to Oracle Autonomous Database
    2. 🧰 Test with Tools
      1. ✅ MCP Inspector
      2. 🤖 Claude Desktop (with mcp-remote)
  8. ⚠️ Limitations and Next Steps
    1. 🔐 OAuth2 Support Limitations
    2. 🚫 Lack of Streamable HTTP and Notifications
    3. 🔧 Future Enhancements
  9. 🧠 Summary

🎯 The Objective

An example of what we want to achieve with the ORDS Concert Application data and an ORDS MCP Server Handler

📍 Introduction

The Model Context Protocol (MCP) is a growing standard for enabling AI tools to interact seamlessly via structured requests. MCP is an open standard that enables AI assistants like Claude, ChatGPT, or Cline to securely connect to external data sources and tools. Think of it as a universal adapter that lets AI models interact with the world beyond their training data.

In this article, we’ll demonstrate how to implement an MCP server directly in Oracle using ORDS (Oracle REST Data Services) and JavaScript MLE (Multilingual Engine). With just a few lines of JavaScript and SQL, you can build an HTTP-based MCP server embedded in your Oracle database. This runs securely, for free, on the hosted Autonomous Database at cloud.oracle.com. Of course, you can choose to run your own setup on-premise too.

This article continues the series on MLE-based handlers—check out earlier examples at peterobrien.blog/tag/multilingual-engine. In particular, we build on concepts introduced in JSON-RPC in ORDS Using MLE JavaScript (Oracle 23ai) and the ORDS Sample Application that is mentioned in 🎉 Data Science in Concert: Inspect Data from the ORDS Sample Application with R!

Here be dragons! Keep in mind that you are using AI tools where access to your database is being permitted. Even if you are running your LLM inside your network, you are still asking an agent to decide on actions based on what it discovers. Be careful with your data and database resources. Only make accessible what is required for the workflow you want to automate. Use the database security and auditing facilities available.

Security, Security, Security

🔍 What Is MCP?

The Model Context Protocol is a JSON-RPC–based interface for exchanging AI capabilities, models, prompts, and resource metadata. Without MCP, AI assistants are like incredibly smart people locked in a room with no internet, phone, or way to access current information or perform actions. They can only work with what they learned during training. MCP breaks down these walls by providing a standardised way for AI models to:

  • Stay current with live information
  • Access real-time data from APIs, databases, and services
  • Control applications and systems
  • Retrieve files and documents
  • Execute actions in the real world

MCP operates on a client-server architecture:

  • MCP Client: The AI assistant (like Cline or Claude)
  • MCP Server: A program that provides specific capabilities (tools, resources, or prompts)

The protocol defines how these communicate through three main primitives:

  • Resources: Static or dynamic content that the AI can reference.
  • Tools: Functions the AI can execute.
  • Prompts: Templates that help the AI understand how to use resources and tools effectively in specific contexts.

The MCP specification states that a server should supports methods such as:

  • initialize: describes the server and its capabilities
  • tools/list: enumerates available tools
  • tools/call: executes a named tool
  • prompts/list: lists available prompts
  • resources/list: lists downloadable resources

That’s not an exhaustive list and they are not all mandatory to implement. In fact, the only one truly required is initialize but you might find an MCP client that expects others to be implemented.

An MCP server can be local, on the same machine running your MCP client (using standard i/o) or remote (using streamable HTTP). The Model Context Protocol server for the Oracle Database is an example of a local MCP server which is accessed using standard i/o. In this article we’ll implement a HTTP based remote server which will not involve any streamable functionality but will support the synchronous request/response aspects of the specification. Note that the most recent version of the MCP specification(Protocol Revision: 2025-06-18) has removed server side events as a supported transport.

MCP clients (like Claude) expect a standard JSON response structure and often use OAuth2 for authentication. Not every authorisation scenario outlined in the specification will be covered in this article.

🚀 Why ORDS + MLE JavaScript?

ORDS can serve as the HTTP interface for all of this functionality — directly from your Oracle database. By combining ORDS with the JavaScript Multilingual Engine (MLE) in Oracle 23ai, you can:

  • Implement MCP server methods entirely in server-side JavaScript.
  • Query live database content when responding to MCP tool calls.
  • Package and publish database-backed tools to AI agents without building a separate backend service.
  • Secure everything with OAuth2 client credentials.

This makes it possible to turn your database into an MCP-compatible API that works with modern AI tooling—without leaving the Oracle environment and without sharing more than you need to.

In this example we’ll provide a get_events tool which lists the results of querying the SEARCH_VIEW in the ORDS_CONCERT_APP schema. That information can be gleaned from the REST Enabled endpoint for that view but your AI Assistant does not have access to that. When coding your own MCP Server through ORDS handlers you can provide access to data as you see fit.

Using the Model Context Protocol server for the Oracle Database will give full access to your database schema and therefore is the most powerful option but you’re unlikely to be sharing those DB credentials with anyone else. Implementing your own MCP Server allows you to greatly restrict that access.

🛠️ MCP Handler Implementation in ORDS

There’s some PL/SQL in this article but it is just the invocation of ORDS APIs to create the user defined resources. The real logic is in JavaScript which executes in the database. You can expand on this and implement your own logic for tools, resources or prompts. Refer to the ORDS mle/handler documentation for more information on that.

Note that although we’re using OAuth2 client credentials flow there are other authorisation options available. This is something that one must choose wisely because there are some aspects of the MCP specification with regard to authorisation discovery which is not currently supported by ORDS. Moreover, there are MCP clients that do not implement or support all the options available. In some cases, such as with Claude desktop, that support may only be available for certain paid subscriptions or premium level plans.

In this article we will protect the ORDS module and have a client credentials OAuth2 client which means we have to request an access token manually and then use it in our MCP server reference. That is not ideal but is a limitation of some of the tools used. More about limitations later.

Here’s the actual ORDS module and handler setup used in our implementation…

🧩 1. Setup – REST enable the database schema and grant MLE privileges

This article is based on the ORDS Sample Application so the database account used is ORDS_CONCERT_APP. It will already be REST Enabled but will require the privileges to execute JavaScript in the database. You can run this as an administrator user, such as ADMIN in the Autonomous Database.

GRANT EXECUTE ON JAVASCRIPT TO ORDS_CONCERT_APP;
GRANT EXECUTE DYNAMIC MLE TO ORDS_CONCERT_APP;

🧱 2. Defining the MCP Module and Template

Now, while logged in as the ORDS_CONCERT_APP user run the following to create the structure for your implementation. This will define a path /mcp/server for the service but it will require a handler for the POST method.

BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'mcp',
p_base_path => '/mcp/');

ORDS.DEFINE_TEMPLATE(
p_module_name => 'mcp',
p_pattern => 'server');

END;
/

✍️ 3. JavaScript MLE Handler Source

The handler responds to MCP methods such as initialize, tools/list, and tools/call. Here I detail the JavaScript portion so that section can be described and explained:

      (req, res) => {
        let response = {
          jsonrpc: "2.0",
          id: null
        };

        try {
          const rpc = req.body;
          response.id = rpc.id ?? null;

          if (rpc.jsonrpc !== "2.0" || !rpc.method) {
            response.error = {
              code: -32600,
              message: "Invalid Request"
            };
          } else if (rpc.method === "initialize") {
            // Respond with server capabilities
            response.result = {
              protocolVersion:"2025-06-18",
              capabilities: {
                tools: {listChanged: false}
              },
              serverInfo: {
                name: "ORDSHandlerExampleServer",
                title: "Example MCP Server implemented in ORDS mle/javascript",
                version: "0.0.1"
              }
            };
          } else if (rpc.method === "tools/list") {
            // Respond with tools list
            response.result = {
                tools: [
                    {
                        name: "get_events",
                        title: "Events Information Provider",
                        description: "Get information on ORDS (Oracle REST Data Services) Sample Application events. The events view brings together artist, venue and schedule concert data. The attributes include EVENT_NAME,ARTIST_NAME,ARTIST_ID,EVENT_ID,EVENT_DATE,EVENT_DETAILS,EVENT_STATUS_NAME,EVENT_STATUS_ID,VENUE_ID,VENUE_NAME,CITY_NAME,MUSIC_GENRES",
                        inputSchema: {
                            type: "object"
                        }
                    }
                ]
            };
          } else if (rpc.method === "tools/call") {
            // Call the get_events tool if that was the tool requested
            if (rpc.params.name === "get_events") {
                const query = ''select * from search_view'';
                const res = session.execute(query);
                textContent = [];
                for (let row of res.rows) {
                  textContent.push({type:"text", text: JSON.stringify(row)});
                };

                response.result = {
                    content: textContent,
                    "isError": false
                };
            } else {
               response.error =  {
                    code: -32602,
                    message: "Unknown tool: invalid_tool_name"
                }
            }
          } else {
            // Unsupported method
            response.error = {
              code: -32601,
              message: "Method not found"
            };
          }

        } catch (e) {
          response.error = {
            code: -32700,
            message: "Parse error",
            data: e.message
          };
        }

        res.status(200);
        res.content_type("application/json");
        res.send(JSON.stringify(response));
      }

At line 2, similar to the /rpc/handler example in the previous article, we define the result structure for a successful response. We are expecting that all requests that we should act on will have an id, so the response as an id field. We copy that across at line 9.

Lines 16 – 28 deal with initialisation which tells the MCP client about the MCP server and its capabilities. In this case we’re saying that the MCP server implementation only provides tools and it does not send notifications if the tool definitions change.

...
} else if (rpc.method === "initialize") {
response.result = {
protocolVersion: "2025-06-18",
capabilities: { tools: { listChanged: false } },
serverInfo: {
name: "ORDSHandlerExampleServer",
title: "Example MCP Server implemented in ORDS mle/javascript",
version: "0.0.1"
}
};
} else ...

Lines 29 – 42 deal with returning the list of tools that the MCP server supports. The description of the tool is important because that is what helps your AI Assistant determine if the tool will be useful for the given workflow.

...
} else if (rpc.method === "tools/list") {
response.result = {
tools: [
{
name: "get_events",
title: "Events Information Provider",
description: "Get information on ORDS (Oracle REST Data Services) Sample Application events. The events view brings together artist, venue and schedule concert data. The attributes include EVENT_NAME,ARTIST_NAME,ARTIST_ID,EVENT_ID,EVENT_DATE,EVENT_DETAILS,EVENT_STATUS_NAME,EVENT_STATUS_ID,VENUE_ID,VENUE_NAME,CITY_NAME,MUSIC_GENRES",
inputSchema: {
type: "object"
}
]
};
} else ...

Lines 43 – 62 are where the real action is. These lines deal with the invocation of the requested tool. In this case the only tool supported is called get_events which is for a specific query on the database. When you are implementing your own MCP server using ORDS handlers you can code this for your own AI integration needs. Note that the specification allows for various content types. For simplicity, in this case, we are constructing an array of text content entries. Although structuredContent could also be used here that content type is not supported by many MCP clients.

...
} else if (rpc.method === "tools/call") {
if (rpc.params.name === "get_events") {
const query = ''select * from search_view'';
const res = session.execute(query);
textContent = [];
for (let row of res.rows) {
textContent.push({type:"text", text: JSON.stringify(row)});
};

response.result = {
content: textContent,
"isError": false
};
} else {
response.error = { code: -32602, message: "Unknown tool: invalid_tool_name" };
}
} else ...

The remainder of the JavaScript code includes placeholders for listing prompts, resources and a little error handling. You can expand on these as you see fit.

Here’s the full handler definition statement to run in your database:

BEGIN
ORDS.DEFINE_HANDLER(
p_module_name => 'mcp',
p_pattern => 'server',
p_method => 'POST',
p_source_type => 'mle/javascript',
p_items_per_page => 0,
p_comments => 'MCP server handler example',
p_source =>
'
(req, res) => {
let response = {
jsonrpc: "2.0",
id: null
};

try {
const rpc = req.body;
response.id = rpc.id ?? null;

if (rpc.jsonrpc !== "2.0" || !rpc.method) {
response.error = {
code: -32600,
message: "Invalid Request"
};
} else if (rpc.method === "initialize") {
// Respond with server capabilities
response.result = {
protocolVersion:"2025-06-18",
capabilities: {
tools: {listChanged: false}
},
serverInfo: {
name: "ORDSHandlerExampleServer",
title: "Example MCP Server implemented in ORDS mle/javascript",
version: "0.0.1"
}
};
} else if (rpc.method === "tools/list") {
// Respond with tools list
response.result = {
tools: [
{
name: "get_events",
title: "Events Information Provider",
description: "Get information on ORDS (Oracle REST Data Services) Sample Application events. The events view brings together artist, venue and schedule concert data. The attributes include EVENT_NAME,ARTIST_NAME,ARTIST_ID,EVENT_ID,EVENT_DATE,EVENT_DETAILS,EVENT_STATUS_NAME,EVENT_STATUS_ID,VENUE_ID,VENUE_NAME,CITY_NAME,MUSIC_GENRES",
inputSchema: {
type: "object"
}
}
]
};
} else if (rpc.method === "tools/call") {
// Call the get_events tool if that was the tool requested
if (rpc.params.name === "get_events") {
const query = ''select * from search_view'';
const res = session.execute(query);
textContent = [];
for (let row of res.rows) {
textContent.push({type:"text", text: JSON.stringify(row)});
};

response.result = {
content: textContent,
"isError": false
};
} else {
response.error = {
code: -32602,
message: "Unknown tool: invalid_tool_name"
}
}
} else {
// Unsupported method
response.error = {
code: -32601,
message: "Method not found"
};
}

} catch (e) {
response.error = {
code: -32700,
message: "Parse error",
data: e.message
};
}

res.status(200);
res.content_type("application/json");
res.send(JSON.stringify(response));
}
');
END;
/



🔐 Securing the MCP Handler

ORDS provides built-in OAuth2 support for securing handlers. In this example, we use the Client Credentials flow, where:

  • Using a client ID and secret request that ORDS issues a bearer token.
  • In your MCP Client configure the MCP Server entry to use that bearer token.
  • Requests to /mcp/server must include an Authorization: Bearer <token> header.

To secure the handler:

DECLARE
L_PRIV_ROLES owa.vc_arr;
L_PRIV_PATTERNS owa.vc_arr;
L_PRIV_MODULES owa.vc_arr;
BEGIN
ORDS.CREATE_ROLE(p_role_name => 'MCPAgentRole');

L_PRIV_ROLES( 1 ) := 'MCPAgentRole';
L_PRIV_MODULES( 1 ) := 'mcp';
ORDS.DEFINE_PRIVILEGE(
P_PRIVILEGE_NAME => 'blog.peterobrien.MCPServerExamplePriv',
P_ROLES => L_PRIV_ROLES,
P_PATTERNS => L_PRIV_PATTERNS,
P_MODULES => L_PRIV_MODULES,
P_LABEL => 'MCP Server Example Privilege',
P_DESCRIPTION => 'Restricts access to the example MCP Server module'
);
END;
/

Make sure the MCP client you use supports this flow (not all do—see Limitations below).

Speaking of clients, we must now define the OAuth2 client which will get access to this protected service. Note that in this case, in order to be able to use the access token for longer, we’re setting the token duration to 8 hours. This example specifies ChangeMe as the secret to use so now is a good time to choose another value or examine the ORDS_SECURITY documentation for other options for generating secrets.

DECLARE
l_client_cred ords_types.t_client_credentials;
BEGIN
l_client_cred := ORDS_SECURITY.REGISTER_CLIENT(
p_name => 'MCPClient',
p_grant_type => 'client_credentials',
p_description => 'MCP Client to the example MCP Server.',
p_client_secret => ords_types.oauth_client_secret(p_secret=>'ChangeMe'),
p_support_email => 'test@example.org',
p_token_duration => 28800);

ORDS_SECURITY.GRANT_CLIENT_ROLE(
p_client_name => 'MCPClient',
p_role_name => 'MCPAgentRole');
COMMIT;
sys.dbms_output.put_line('CLIENT_ID:' || l_client_cred.client_key.client_id);
sys.dbms_output.put_line('CLIENT_SECRET:' || l_client_cred.client_secret.secret);
END;
/

That will create a client and output the generated CLIENT_ID and CLIENT_SECRET. You will have to remember these values because they are needed to get an access token. This can be achieved through curl:

curl -i -k --user <client_id>:<client_secret> --data "grant_type=client_credentials" https://<my ORDS server>/ords/ords_concert_app/oauth/token

Replace the variables as appropriate. For example:

curl -i -k --user IV2YsD5Z0sr8_Wvgd0U1jQ..:ChangeMe --data "grant_type=client_credentials" https://c9abzixuw5nq9si-kop3yqis71qcmbx2.adb.eu-frankfurt-1.oraclecloudapps.com/ords/ords_concert_app/oauth/token

That will give you an access token which you will use later.

{
"access_token":"8kbzUqtgsDrCTNKB6Xb32w",
"token_type":"bearer",
"expires_in":28800
}

🧪 Deploying and Testing the MCP Server

🛰️ Deploy to Oracle Autonomous Database

This handler works with Autonomous Database using their hosted ORDS with 23ai. That’s the simplest and most convenient to get started with this example although you can run this on your own infrastructure.

🧰 Test with Tools

You can test your MCP server using the following tools but there are other MCP clients available:

  • MCP Inspector: Enter the /mcp/server URL and token to explore responses interactively.
  • 🤖 Claude Desktop (with mcp-remote): Add your server as a remote tool and use get_events inside Claude chat.

✅ MCP Inspector

The MCP Inspector is a browser based tool for testing and debugging MCP servers. It provides an interactive interface and proxy mechanism that simplifies the development, inspection, and validation of MCP servers before integrating them with AI assistants or production systems. It is the best place to start verifying your handler.

It requires Node.js and is as easy to start as npx @modelcontextprotocol/inspector

Start the inspector – it hosts a mini web server and opens a browser to show the UI
In the UI enter the MCP server address – This is the full path for mine hosted in Frankfurt region
Set the Authorization header with the access token as Bearer Token value
The inspector tool invoking your hosted MCP Server implementation

With the Authentication details provided just press the Connect button. As you can see from the above recording you get live data from your MCP Server mle/handler implementation. The invocation of the relevant methods initialize, tools/list and tools/call can be seen in the history.

Now that you know the MCP Server implementation works and complies with the specification it is time to plug this into an AI Assistant which can operate as an MCP Client. One example is Claude Desktop.

🤖 Claude Desktop (with mcp-remote)

Claude is an AI assistant created by Anthropic. It is built to assist with a wide variety of tasks – from answering questions and helping with analysis to writing, coding, math, creative projects, and having conversations on a wide range of topics. Claude.ai refers to the online version of the AI chatbot, while Claude Desktop is a dedicated application for Windows and macOS that allows users to access Claude’s features directly on their computer, offering a more streamlined and efficient experience. Although still in beta mode…Claude Desktop can connect to remote MCP servers, allowing you to access tools and data hosted on the internet. This integration enhances Claude’s capabilities by enabling it to perform tasks using resources from various external services. However, it does have its limitations. For example, although the free version works with local MCP servers to use remote MCP servers directly requires an upgrade to a paid plan.

To work around that we’ll use mcp-remote which will act as a local MCP server proxying to your remote server.

Manage your list of MCP Servers in the Developer section of Claude Desktop Settings

The Edit Config button will open up a file explorer window with your claude_desktop_config.json file selected. Edit that file and add an entry for your MCP Server.

{
  "mcpServers": {
    "ORDS-handler-example": {
      "command": "npx",
      "args": [
	"mcp-remote",
	"https://c4tozyxuw8nq2ja-kgl2qyis29qcbmv2.adb.eu-frankfurt-1.oraclecloudapps.com/ords/ords_concert_app/mcp/server",
	"--header",
	"Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer 8kbzUqtgsDrCTNKB6Xb32w"
      }
    }
  }
}

Note that that you will have to specify your access token and restart Claude Desktop to pick up the change. In the above example the ORDS-handler-example is defined to execute the npx command to run the mcp-remote Node.js package locally. Additional command parameters include the URL for the remote MCP Server and an instruction to include an Authorization header. Note that the access token from earlier is used here.

Once Claude Desktop is restarted, let’s start asking questions. The AI Agent knows that it has an MCP Server available to it that has a tool for retrieving concert events.

At startup Claude Desktop got the list of tools available from the MCP Server

Note that it is best practice to not give an agent unsupervised access to your tools and resources so in the majority of cases, select “Allow once” so that each action is reviewed before executing.

So let’s ask Claude a question:
From the ORDS Sample Application concert data who are the most popular artists with sold out concerts?

Claude gets the live data and performs its analysis on it

And that’s it! You now can now chat with an AI Assistant to gain insight on your ORDS Sample Application concert events, venues and artists.


⚠️ Limitations and Next Steps

🔐 OAuth2 Support Limitations

Some MCP clients—such as Claudedo not fully support OAuth2 client credentials at the time of writing. This may require:

  • Temporarily disabling OAuth2 for local testing.
  • Creating a public ORDS endpoint for development use.

Neither of which are really viable options. Remember the security, security, security statement at the top!

ORDS does not currently provide support for the OAuth2 flow to list identity providers and that reduces the integration options. The approach mentioned in this article to have an access token that lasts longer than the default 1 hour is not practical in the long term. However, you may find an MCP client that works with one of the other OAuth2 flows.

🚫 Lack of Streamable HTTP and Notifications

One limitation of this ORDS mle/javascript MCP server implementation is the absence of streamable HTTP responses and real-time notifications.

  • Streamable HTTP: The MCP specification allows servers to return results progressively, which is useful for long-running operations or large responses. ORDS handlers currently return only complete responses, meaning agents must wait until all processing finishes before receiving data.
  • Notifications: MCP also supports server-initiated notifications to inform clients of changes (e.g., new events becoming available). At present, ORDS does not provide a mechanism for pushing such asynchronous messages, so clients must re-poll endpoints to detect updates.

While these limitations do not prevent basic MCP functionality, they reduce efficiency for agents expecting real-time updates or streamed results.

🔧 Future Enhancements

Here are some possible next steps to build on this example mle/javascript handler:

  • Add a tool for managing events
  • Implement dynamic prompt generation in prompts/list.
  • Add support for downloading resources.
  • Log incoming MCP requests in a table for auditing.
  • Move the logic into an MLE Module so that the JavaScript code is better structured.
  • Explore the use of different OAuth2 security flows which will work with you chosen MCP client.

🧠 Summary

With just a few lines of MLE JavaScript and an ORDS handler definition, you can turn your Oracle database into a lightweight MCP HTTP server.

This approach allows Oracle developers to publish tools, prompts, and data to modern AI tools like Claude—without deploying external services. Moreover, the data and database resources that are shared is limited to what is specifically coded for in your handler.

📚 Continue exploring: More MLE Articles, https://github.com/oracle/mcp and Model Context Protocol server for the Oracle Database

Mustache Templates as a Service with ORDS

  1. Introduction
    1. Step 1: Setting up the Database Table for Mustache Templates
    2. Step 2: Defining ORDS services for Mustache Template creation, retrieval and use
      1. Define the ORDS REST module
      2. Define the ORDS REST templates
      3. GET: /templates/
      4. POST: /templates/
      5. GET: /templates/{id}
      6. PUT: /templates/{id}
      7. POST: /templates/{id}/generate
    3. Step 3: Protecting the Services
    4. Step 4: Some Postman OAuth token magic
  2. For your convenience
  3. Conclusion

Introduction

In a previous post, we explored how to use the Mustache template engine with ORDS mle/javascript Handlers to transform SQL query responses into application/json responses. If you missed that, check it out – Transform your SQL Results with Mustache and ORDS JavaScript Handlers.

The idea was inspired by a tweet from @FriedholdMatz and I put together an initial implementation at the time which I have since refined.

This follow-up dives deeper into the concept of using Mustache as a service, enabling users to submit their own template definitions and payloads for dynamic transformations. This service can be incredibly useful when you need to allow external users or applications to define how they want data structured without modifying your code every time.

In fact, this article is going to cover a wide range of subjects related to developing services with ORDS. These include:

  • Use of implicit parameters both in query, PL/SQL and mle/javascript handlers.
  • Protecting all services defined in a module using a single privilege definition.
  • Invoking the secured services from Postman using OAuth 2.0 Client Credentials to automatically obtain a new bearer token.

Let’s walk through the implementation steps. As a REST Enabled user connect to your 23ai database where you have already defined your MLE library for Mustache as covered in the previous article. Just like in that article, our REST Enabled user in this article is the HR schema and all statements are executed in the database as that user.

NOTE! The following steps are based on Oracle 23ai MLE database objects which you should have created from Transform your SQL Results with Mustache and ORDS JavaScript Handlers. Having the MLE Module for Mustache and an MLE library making it available to mle/javascript handlers is a prerequisite.

Step 1: Setting up the Database Table for Mustache Templates

First, we need a database table to store the template definitions. This table will allow clients to create and manage their templates, identified by a unique ID.

CREATE TABLE mustache_templates (
    template_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    template_name VARCHAR2(100) not null,
    template_text CLOB not null,
    template_owner VARCHAR2(200) not null,
    template_content_type VARCHAR2(100) not null,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

In this table, template_name is used for a meaningful description of the Mustache template, while template_text will store the Mustache template itself. You will also notice a template_owner column which we will use to keep the rows distinct for each template owner. Put simply, a user, identified by their OAuth client identifier, manages their own templates. Both the primary key template_id and the created_at columns are automatically assigned values on insert.


Step 2: Defining ORDS services for Mustache Template creation, retrieval and use

Now, we need an API endpoint to submit, retrieve and use templates. Access to each row is going to be restricted based on the OAuth client identifier so we will require a slightly more complex set of logic than what REST Enabling the mustache_templates table will provide. That will all be taken care of in one module with base path = /templates/ and a few of REST Service templates defined. These are:

  • /templates/. – This will have a GET handler to retrieve all rows the user has access to and a POST handler to add new Mustache templates.
  • /templates/:id – This will have a GET handler to retrieve a specific Mustache template by template_id and a PUT handler to update that specific Mustache template.
  • /templates/id/generate – This will have a single POST handler to apply the Mustache template to the request body and return the generated content.

The GET handlers will be simple query handlers. The PUT and handler will involve a PL/SQL block. One of the POST handlers will be defined using PL/SQL and the other will use JavaScript. In all cases they will use the ORDS provided implicit parameter for referencing the current user which will be the Client ID of the OAuth client used at runtime.

Define the ORDS REST module

This PL/SQL block will create the module which all the service handlers will belong to.

BEGIN
  ORDS.DEFINE_MODULE(
      p_module_name    => 'templates',
      p_base_path      => '/templates/',
      p_items_per_page => 25,
      p_status         => 'PUBLISHED');
  COMMIT;
END;

Define the ORDS REST templates

ORDS uses templated REST Service definitions and we will create three now:

BEGIN
  ORDS.DEFINE_TEMPLATE(
      p_module_name    => 'templates',
      p_pattern        => '.',
      p_comments       => 'Retrieve all Mustache templates the runtime user has access to and create new ones');

  ORDS.DEFINE_TEMPLATE(
      p_module_name    => 'templates',
      p_pattern        => ':id',
      p_comments       => 'Retrieve or update a specific Mustache template');

  ORDS.DEFINE_TEMPLATE(
      p_module_name    => 'templates',
      p_pattern        => ':id/generate',
      p_comments       => 'Transform the request payload using the specified Mustache template');

  COMMIT;
END;

With these in place the next step is to define the relevant handlers for the GET, PUT and POST methods.

GET: /templates/

This will retrieve all Mustache template records that the runtime user has access to. This query uses the implicit parameter :current_user and a special column alias $.id which is used to generate a self reference link in the response.

BEGIN
  ORDS.DEFINE_HANDLER(
      p_module_name    => 'templates',
      p_pattern        => '.',
      p_method         => 'GET',
      p_source_type    => 'json/collection',
      p_source         => 
'select TEMPLATE_ID as "$.id",
TEMPLATE_ID, TEMPLATE_NAME, TEMPLATE_CONTENT_TYPE, TEMPLATE_TEXT, CREATED_AT from MUSTACHE_TEMPLATES where TEMPLATE_OWNER = :current_user');
  COMMIT;
END;

The above handler will return a response similar to this when invoked by an authorised user:

 {
    "items": [
        {
            "template_id": 1,
            "template_name": "Employees",
            "template_content_type": "text/xml",
            "template_text": "<employees>{{#rows}} <employee id=\"{{EMPLOYEE_ID}}\" first_name=\"{{FIRST_NAME}}\" last_name=\"{{LAST_NAME}}\" salary=\"{{SALARY}}\"/>{{/rows}}</employees>",
            "created_at": "2024-10-06T20:33:43.816865Z",
            "links": [
                {
                    "rel": "self",
                    "href": "https://ords.example.com/ords/hr/templates/1"
                }
            ]
        },
        {
            "template_id": 2,
            "template_name": "Baeldung Inverted Sections",
            "template_content_type": "text/xml",
            "template_text": "{{#todos}} <h2>{{title}}</h2> {{/todos}} {{^todos}} <p>No todos!</p> {{/todos}}",
            "created_at": "2024-10-06T20:37:39.144667Z",
            "links": [
                {
                    "rel": "self",
                    "href": "https://ords.example.com/ords/hr/templates/2"
                }
            ]
        },
        {
            "template_id": 3,
            "template_name": "Tsmean Mustache Example",
            "template_content_type": "text/plain",
            "template_text": "{{#users}} {{.}} {{/users}}",
            "created_at": "2024-10-06T21:13:40.682858Z",
            "links": [
                {
                    "rel": "self",
                    "href": "https://ords.example.com/ords/hr/templates/3"
                }
            ]
        }
    ],
    "hasMore": false,
    "limit": 25,
    "offset": 0,
    "count": 3,
    "links": [
        {
            "rel": "self",
            "href": "https://ords.example.com/ords/hr/templates/"
        },
        {
            "rel": "edit",
            "href": "https://ords.example.com/ords/hr/templates/"
        },
        {
            "rel": "describedby",
            "href": "https://ords.example.com/ords/hr/metadata-catalog/templates/"
        },
        {
            "rel": "first",
            "href": "https://ords.example.com/ords/hr/templates/"
        }
    ]
}

POST: /templates/

This handler is a PL/SQL handler which will insert a new Mustache template record. This handler has some content of note.

  • The implicit parameter :current_user is used to limit queries to rows that the runtime user has access to.
  • Once the runtime user has 10 Mustache templates they can not create any more.
  • The implicit parameters :status_code and :forward_location are used to generate a reference to the just inserted Mustache template. ORDS will generate a response body containing the json representation of that new record.
  • You will notice that parameters are not defined for :template_name, :template_content_type and :template_text. When not explicitly defined they are assumed to be fields in the request body.
  • The p_mimes_allowed argument makes it clear that application/json payloads are expected by this handler.
BEGIN
  ORDS.DEFINE_HANDLER(
      p_module_name    => 'templates',
      p_pattern        => '.',
      p_method         => 'POST',
      p_source_type    => 'plsql/block',
      p_mimes_allowed  => 'application/json',
      p_source         => 
'DECLARE
  existing_templates_count NUMBER;
  new_template_id NUMBER;
BEGIN
    select count(*) into existing_templates_count from mustache_templates where TEMPLATE_OWNER = :current_user;
    if existing_templates_count < 10 then
      insert into MUSTACHE_TEMPLATES (TEMPLATE_NAME, TEMPLATE_CONTENT_TYPE, TEMPLATE_TEXT, TEMPLATE_OWNER) VALUES(:template_name, :template_content_type, :template_text, :current_user)
      RETURNING template_id INTO new_template_id;
      :status_code := 201;
      :forward_location := new_template_id;
    else
-- Too many records
       :status_code := 400;
    end if;
  END;');
  COMMIT;
END;

This handler will take a request payload such as the following…

{
            "template_name": "Deployment YAML",
            "template_content_type": "application/yaml",
            "template_text": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{app_name}}\nspec:\n  replicas: {{replicas}}\n  selector:\n    matchLabels:\n      app: {{app_label}}\n  template:\n    metadata:\n      labels:\n        app: {{app_label}}\n    spec:\n      containers:\n      - name: {{container_name}}\n        image: {{image}}\n        ports:\n        - containerPort: {{port}}\n"
}

…and return a response like this…

HTTP/1.1 201 Created
Date: Tue, 08 Oct 2024 21:08:08 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Content-Location: https://ords.example.com/ords/hr/templates/21
ETag: "jzuprj2x/Ns2nMQhQ9frDCwWLZH7T+B3S3klPT8FpP3MFejzkndlqsqFV80rx85Auh5N83fF5mKFb4sZd+haEg=="
Location: https://ords.example.com/ords/hr/templates/21
 
{
  "template_id":21,
  "template_name":"Deployment YAML",
  "template_content_type":"application/yaml",
  "template_text":"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{app_name}}\nspec:\n replicas: {{replicas}}\n selector:\n matchLabels:\n app: {{app_label}}\n template:\n metadata:\n labels:\n app: {{app_label}}\n spec:\n containers:\n - name: {{container_name}}\n image: {{image}}\n ports:\n - containerPort: {{port}}\n","created_at":"2024-10-08T21:08:07.993387Z",
  "links":[
    {
      "rel":"self",
      "href":"https://ords.example.com/ords/hr/templates/21"
    },
    {
      "rel":"edit",
      "href":"https://ords.example.com/ords/hr/templates/21"
    }, 
    {
      "rel":"describedby",
      "href":"https://ords.example.com/ords/hr/metadata-catalog/templates/item"
    },
    {
      "rel":"collection",
      "href":"https://ords.example.com/ords/hr/templates/"
    }
  ]
}

GET: /templates/{id}

This will retrieve a Mustache template by its unique identifier. Similar to the GET /templates/ handler it is a simple query but uses the :current_user implicit parameter and the $.id alias for generating a self link in the response. Note that the value for :id in the query comes from the URL path pattern of the template.

BEGIN
  ORDS.DEFINE_HANDLER(
      p_module_name    => 'templates',
      p_pattern        => ':id',
      p_method         => 'GET',
      p_source_type    => 'json/item',
      p_source         => 
'select 
   TEMPLATE_ID as "$.id",
   TEMPLATE_ID, 
   TEMPLATE_NAME, 
   TEMPLATE_CONTENT_TYPE, 
   TEMPLATE_TEXT, 
   CREATED_AT 
from MUSTACHE_TEMPLATES 
where TEMPLATE_OWNER = :current_user and TEMPLATE_ID = :id');

COMMIT;
END;

This returns the specific Mustach template requested so one can see what it produces based on the content in the template_text. Hopefully the template_name is descriptive too 😀

PUT: /templates/{id}

This handler will update an existing Mustache template if the runtime user has access to the row. Similar to the POST /templates/ handler it uses implicit parameters for the SQL query but also to instruct ORDS on how to generate the response.

BEGIN
  ORDS.DEFINE_HANDLER(
      p_module_name    => 'templates',
      p_pattern        => ':id',
      p_method         => 'PUT',
      p_source_type    => 'plsql/block',
      p_source         => 
'BEGIN
    update MUSTACHE_TEMPLATES set 
    TEMPLATE_NAME = :template_name,
    TEMPLATE_CONTENT_TYPE = :template_content_type,
    TEMPLATE_TEXT = :template_text
    where template_id = :id and TEMPLATE_OWNER = :current_user;
    :status_code := 200;
    :forward_location := :id;
END;');
  COMMIT;
END;

POST: /templates/{id}/generate

And now the mle/javascript handler that will apply the Mustache template to the request body to return a generated document. The request payload will be the data to be used into the Mustache template which is specified by the templates identifier. This endpoint will:

  1. Retrieve the template corresponding to template_id.
  2. Use the JavaScript Mustache engine to transform the payload.

Similar to the mle/javascript handler in the previous article it relies on an MLE environment to reference the Mustache template. Other items to note about this mle/javascript handler:

  • Checks the content_type implicit parameter and returns an appropriate status code if it is not application/json.
  • Provides fetchInfo to database session so that it knows how to represent CLOB values from the TEMPLATE_TEXT column.
  • Refers to the :id parameter using the uri_parameters of the request.
  • Refers to the current_user implicit parameter

The syntax for referring to this parameters is covered in the ORDS Developer Guide – Manually Creating RESTful Services Using Javascript.

  ORDS.DEFINE_HANDLER(
      p_module_name    => 'templates',
      p_pattern        => ':id/generate',
      p_method         => 'POST',
      p_source_type    => 'mle/javascript',
      p_mle_env_name   => 'LIBRARY_ENV',
      p_source         => 
' 
 (req, resp) => {
    if (''application/json''.localeCompare(req.content_type) != 0) {
       resp.status(415);
    } else {
        const requestBody = req.body;
          const template_query = ''select * from mustache_templates where template_id = :1 and template_owner = :2'';
          const options = { fetchInfo: { TEMPLATE_TEXT: { type: oracledb.STRING } } };
          const template_definition = session.execute(template_query, [req.uri_parameters.id, req.current_user], options);
          if (template_definition.rows.length != 1) {
            resp.status(404);
          } else {
                const mustache = await import(''mustache'');

                var output = mustache.default.render(template_definition.rows[0].TEMPLATE_TEXT, requestBody);
                resp.content_type(template_definition.rows[0].TEMPLATE_CONTENT_TYPE);
                resp.status(200);
                resp.send(output);
          }
    }
}
');

Invoking this service specifying the Mustache template id in the URL will transform the request payload into a generated document using the specified template. It will even set the response content type as specified by the Mustache template.

Using the “Inverted Sections” Mustache template example from https://www.baeldung.com/mustache with Todo entries.
Using the “Inverted Sections” Mustache template example from https://www.baeldung.com/mustache with zero entries.

The eagle eyed reader will have noticed that this mle/javascript handler source is checking that the content type of the request is application/json and strictly speaking the Mustache Templating Engine can work with more than JSON as the source context. As an additional homework exercise you could explore working with different request payload structures.


Step 3: Protecting the Services

For this service we’re going to restrict access to only authenticated users. Note that every handler refers to the implicit parameter :current_user. Every row will belong to a specific template_owner user and that will correspond to an OAuth client that we can issue for each prospective user. To achieve that a Role must be defined and a Privilege explicitly protecting the module created earlier is required. Run this…

DECLARE
L_PRIV_ROLES owa.vc_arr;
L_PRIV_PATTERNS owa.vc_arr;
L_PRIV_MODULES owa.vc_arr;
BEGIN

ORDS.CREATE_ROLE(
    P_ROLE_NAME => 'blog.peterobrien.MustacheTemplateUser'
);

L_PRIV_MODULES( 1 ) := 'templates';
L_PRIV_ROLES( 1 ) := 'blog.peterobrien.MustacheTemplateUser';

ORDS.DEFINE_PRIVILEGE(
    P_PRIVILEGE_NAME => 'blog.peterobrien.MustachTemplate',
    P_ROLES => L_PRIV_ROLES,
    P_PATTERNS =>  L_PRIV_PATTERNS,
    P_MODULES => L_PRIV_MODULES,
    P_LABEL => 'Mustache Template Privilege',
    P_DESCRIPTION => 'Protects access to the Mustache Template module',
    P_COMMENTS=> 'Mustache Template module provides the managed and use of Mustache Template as a Service.'
);
COMMIT;
END;

Send a request to one of the handlers now and one should get a HTTP 401 response.

GET /ords/hr/templates/ HTTP/1.1
User-Agent: PostmanRuntime/7.41.0
Accept: */*
Cache-Control: no-cache
Host: ords.example.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.1 401 Unauthorized
Date: Tue, 08 Oct 2024 21:49:45 GMT
Content-Type: text/html
Content-Length: 451668
Connection: keep-alive

With the module now directly protected let’s create an OAuth client that can access it. Note that this is using OAUTH package but ORDS is moving to a new OAUTH_SECRETS package which I will refer to here when the documentation for it is published…

BEGIN
    ORDS_METADATA.OAUTH.CREATE_CLIENT(
        P_NAME => 'TemplateClient1',
        P_GRANT_TYPE => 'client_credentials',
        P_OWNER => 'HR',
        P_DESCRIPTION => 'A client for the Mustache Template as a Service module',
        P_SUPPORT_EMAIL => 'test@example.com',
        P_PRIVILEGE_NAMES => 'blog.peterobrien.MustachTemplate'
    );
    ORDS_METADATA.OAUTH.GRANT_CLIENT_ROLE(
        P_CLIENT_NAME => 'TemplateClient1',
        P_ROLE_NAME => 'blog.peterobrien.MustacheTemplateUser'
    );
    COMMIT;
END;

Execute a select statement on USER_ORDS_CLIENTS to get the CLIENT_ID and CLIENT_SECRET for this TemplateClient1.

select 
   client_id, client_secret 
from user_ords_clients 
where name = 'TemplateClient1';

With that client_id and client_secret you can get an access token which can then be used to invoke the Mustache template services we have created. The :current_user value in our handlers will be the client_id value from the request.

For more information on protecting access to your ORDS services see the Developer Guide – Configuring Secure Access to RESTful Services.

Step 4: Some Postman OAuth token magic

The access token will expire after an hour and another one will have to be requested. Rather than going through all that manually we can use our REST client to manage that, even automatically requesting a new access token when the current one expires. In this article we’ll use Postman but there are alternatives. In fact if you are coding a client application to use the Mustache Template as a Service endpoints then a similar framework for handling the OAuth token lifecycle will be required. For Postman, we’ll define our authorisation flow and requests in a collection called Mustache.

In “My Workspace” I have a collection called Mustache

The collection is useful for keeping important metadata in one place such as how authentication and authorisation will be achieved…

In the Authorization definition of my Mustach collection I specify that I will use OAuth 2.0 as the Auth Type
Keep on scrolling down to enter specifics.

Let’s look at some of those specifics…

  • The token name doesn’t really matter. You can use any name you want.
  • The grant type should match your ORDS OAuth client definition: Client Credentials
  • The access token URL will be the full URL for your REST Enabled schema’s access endpoint. Generally that is <server>/ords/<schema alias>/oauth/token For example: https://ords.example.com/ords/hr/oauth/token
  • The client ID and client secret comes from the query on user_ords_clients earlier.
  • The client authentication should be Send as Basic Auth header. Postman will send the Client ID and Client Secret as basic authentication to the Access Token URL endpoint to get an access token.
Keep on scrolling down and press that Get New Access Token button to verify it all works.

With that in place then every request defined in the Mustache collection can just inherit that definition from the collection and the OAuth client token lifecycle is all automagically taken care of.

Of course, I’m running all this on my Oracle Free Tier 23ai Autonomous Database in Frankfurt

For your convenience

The ORDS module / template / handler and OAuth client definition as well as the Postman collection definition are available at https://gist.github.com/pobalopalous/1d9ad8c36d81074bb9af146dc95dfe42 so you can get up and running fast. 🚀

Note that if you import that Mustache Collection into Postman the hostname referred to will be ords-23ai.adb.eu-frankfurt-1.oraclecloudapps.com which doesn’t exist. You will have to change those entries to point to your own system.

Don’t want to set this up yourself but still want to try it out? Leave a comment below and I’ll get back to you with your own Client ID and Client Secret for you to use on a trial basis for a few days. All I have to do is create a new OAuth client with the same privilege and role and then forward you the Client ID and Client Secret. After a few days I can just delete the OAuth client.


Conclusion

By extending ORDS with Mustache templates as a service, you now have a flexible system that allows users to dynamically define and utilize templates. This can be a practical, reusable tool for building customisable reporting, notifications, or any scenario where the structure of data needs to be adaptable.

Feel free to experiment with more complex templates and explore how Mustache’s logic-less approach can simplify many data transformation tasks in your applications.

Stay tuned for future articles where we dive deeper into templating and ORDS functionalities!


If you have any questions or feedback, drop a comment below.