Build Model Context Protocol servers
OpenAF can expose Model Context Protocol tools through regular oJobs. The https://github.com/openaf/mini-a/mcps directory in the distribution contains working examples (mcp-db.yaml, mcp-ch.yaml, …) and the shared runtime lives in https://github.com/openaf/oJob-common/oJobMCP.yaml. This guide distils the key pieces so you can author your own MCP server and test it locally or over HTTP.
Understand the shared MCP runtime
oJob-common/oJobMCP.yaml publishes two shortcuts that do most of the heavy lifting:
httpdMCPstarts an HTTP JSON-RPC endpoint (defaults to/mcp) and wires requests to jobs you expose.stdioMCPspeaks MCP over STDIO, allowing you to run the server as a child process.
Both variants expect:
description.serverInfowithname,titleandversion.fnsMeta: MCP tool metadata (JSON Schema, annotations, etc.).fns: the mapping between MCP tool names and oJob job names.
Your YAML file only needs to prepare these maps and include the shortcut.
Start from the MCP skeleton
Create a new file under mini-a/mcps/, e.g. mcp-myservice.yaml, following the structure below (adapted from mini-a/mcps/CREATING.md):
# Author: You
help:
text : A STDIO/HTTP MCP server for MyService
expects:
- name : onport
desc : If defined starts an HTTP MCP server on the provided port
example : "8888"
mandatory: false
- name : myConfig
desc : Example extra parameter
example : "value"
mandatory: true
todo:
- Init MyService
- (if ): "isDef(args.onport)"
((then)):
- (httpdMCP): &MCPSERVER
description:
serverInfo:
name : mini-a-myservice
title : OpenAF MCP MyService
version: 1.0.0
((fnsMeta)): &MCPFNSMETA
myservice-tool:
name : myservice-tool
description: Performs the main operation
inputSchema:
type : object
properties:
param1:
type : string
description: Example parameter
required: [ param1 ]
annotations:
title : MyService Tool
readOnlyHint : true
idempotentHint: true
((fns )): &MCPFNS
myservice-tool: MyService Tool
((else)):
- (stdioMCP): *MCPSERVER
((fnsMeta)): *MCPFNSMETA
((fns )): *MCPFNS
ojob:
opacks:
- openaf : 20250915
- oJob-common: 20250914
daemon : true
argsFromEnvs: true
logToConsole: false
include:
- oJobMCP.yaml
jobs:
- name : Init MyService
check:
in:
myConfig: isString
exec : | #js
// Initialise SDK clients, cache credentials, etc.
global.myServiceConfig = args
- name : MyService Tool
check:
in:
param1: isString
exec : | #js
if (!isDef(global.myServiceConfig)) return "[ERROR] Service not initialised"
return {
content: [{
type: "text",
text: `Processed ${args.param1}`
}]
}
- name : Cleanup MyService
type : shutdown
exec : | #js
delete global.myServiceConfig
Key points:
- Keep the
helpsection up to date so users know which arguments are required. - The
todoblock first runs any initialisation job(s) and then chooses between HTTP or STDIO mode using theonportargument. fnsMetaentries must describe your tools with valid JSON Schema;fnsmaps the tool names to the job that should run.- Always add a shutdown job for cleanup — it runs when the MCP exits.
Tips for implementing tools
- Return errors as strings that start with
[ERROR] …. The existing MCP tooling looks for that pattern. - Use
check.invalidations so bad inputs fail fast. - For read/write operations, consider a flag (e.g.
allowWrite) to prevent accidental changes, similar tomcp-ssh.yaml. - Inspect other MCPs (database, time, SSH, etc.) in
mini-a/mcps/for real-world patterns.
Smoke-test the server with oJob
Run in STDIO mode during development:
ojob mcps/mcp-myservice.yaml myConfig=value
Run as an HTTP server:
ojob mcps/mcp-myservice.yaml onport=12345 myConfig=value
Both rely on the shortcuts from oJob-common/oJobMCP.yaml. If you need extra HTTP middleware, add it before the httpdMCP call.
Test with $mcp inside OpenAF
openaf/js/openaf.js ships the $mcp client helper. You can use it from the REPL or inside your scripts:
var client = $mcp({
cmd : "ojob mcps/mcp-myservice.yaml myConfig=value",
debug : false,
strict: true
})
client.initialize()
log(client.listTools()) // -> { tools: [...] }
var result = client.callTool("myservice-tool", { param1: "demo" })
log(result)
client.destroy()
For HTTP mode, switch to type: "remote" and provide url: "http://localhost:12345/mcp".
Test with oafp in=mcp
oafp can act as a CLI MCP client (see oafp/src/docs/USAGE.md):
-
List tools exposed by your STDIO server:
oafp in=mcp inmcptoolslist=true data="(cmd: 'ojob mcps/mcp-myservice.yaml myConfig=value')" -
Invoke a tool via STDIO:
oafp in=mcp data="(cmd: 'ojob mcps/mcp-myservice.yaml myConfig=value', tool: 'myservice-tool', params: (param1: 'demo'))" -
Invoke the same tool over HTTP:
oafp in=mcp data="(type: remote, url: 'http://localhost:12345/mcp', tool: 'myservice-tool', params: (param1: 'demo'))"
Set inmcplistprompts=true to inspect available prompts (if you expose any) or use the other connection fields (__timeout__, __clientInfo__, …) listed in the oafp usage document.
Tool blacklist
You can prevent specific tools from being accessible to callers by providing a blacklist array in the MCP client options:
var client = $mcp({
cmd : "ojob mcps/mcp-myservice.yaml myConfig=value",
blacklist: ["dangerous-tool", "internal-only-tool"]
})
client.initialize()
// "dangerous-tool" will not appear in listTools() or be callable
The blacklist is applied at the client side — listed tool names are filtered out from listTools responses and calls to them are blocked before being forwarded to the server.
Dummy mode and oJob-based servers
$mcp and $jsonrpc support two additional server types for testing and oJob integration:
Dummy mode
A dummy server returns empty/no-op responses to all tool calls. Useful for integration testing:
var client = $mcp({ type: "dummy" })
client.initialize()
// all tool calls return empty results — no real process is spawned
oJob-based servers
Point $mcp directly at an oJob YAML file as the server:
var client = $mcp({
type : "ojob",
ojob : "mcps/mcp-myservice.yaml",
args : { myConfig: "value" }
})
client.initialize()
A default command directory can also be set so relative process paths resolve correctly:
var client = $mcp({
cmd : "ojob mcp-myservice.yaml myConfig=value",
cmdDir : "/path/to/mcps"
})
Per-call options
Both $jsonrpc and $mcp accept optional per-call options to override tool execution parameters on individual calls:
var result = client.callTool("myservice-tool", { param1: "demo" }, {
timeout: 30000 // override timeout for this call only
})
TOON output format
MCP server text responses can be formatted using the TOON terminal rendering library. Use the toon format type with $output to produce rich terminal output:
$output(myData, { format: "toon" })
When returning tool results, you can generate TOON-formatted text that MCP clients supporting rich terminal output will render:
return {
content: [{
type: "text",
text: $output(myData, { format: "toon", retStr: true })
}]
}
Server info with getInfo
Both JSON-RPC and MCP client objects expose a getInfo() method to retrieve server metadata after initialisation:
var client = $mcp({ cmd: "ojob mcps/mcp-myservice.yaml myConfig=value" })
client.initialize()
var info = client.getInfo()
log(info) // -> { name: "mini-a-myservice", version: "1.0.0", ... }
Ping support (stdio)
The MCP stdio server now handles ping requests automatically, returning the expected pong response for liveness checks. No additional configuration is required.
Where to go next
- Browse the catalog in
https://github.com/openaf/mini-a/mcps/README.mdfor inspiration. - Revisit
https://github.com/openaf/mini-a/mcps/CREATING.mdwhenever you need the full checklist. - Extend
https://github.com/openaf/oJob-common/oJobMCP.yamlif you need new shared behaviours (additional logging, authentication, etc.).
October protocol and authentication additions
The server API reference documents shared protocol helpers, stdio serving, and HTTP replies. Modern stateless protocol support is negotiated explicitly by clients; see $mcp. For delegated client login, use the localhost OAuth guide.