Oracle Forms
io.github.aoreshkovPublished by GitHub account aoreshkovv0.20.0Updated Nov 7, 2026
Serves Oracle Forms module content (.fmb/.mmb/.pll/.olb) from a directory to MCP clients.
Overview
Serves Oracle Forms modules (.fmb/.mmb/.pll/.olb) from a local directory so an assistant can inspect blocks, triggers, PL/SQL and cross-module references.
- What you can do
- List, convert and index Forms modules, with overviews of sections, windows and canvases
- Inspect blocks, items, property classes, master-detail relations and base-table columns
- Read triggers and program units, including inherited bodies resolved from parent modules
- Search PL/SQL or raw XML within one module or across every cached module
- Read line ranges of converted files and raw XML fragments of any object
- Record, search and remove annotations and cross-reference relations about elements
- When to use it
- Useful when you need to understand or document an existing Oracle Forms codebase: tracing where a form is called from, following a :GLOBAL value, finding which modules subclass a library, or mapping a block's triggers before a migration or port.
- Requirements
- Runs locally over stdio (or HTTP) from a Docker image, an .mcpb bundle, a Claude Code plugin or a source build. Needs a forms directory (--forms-dir or OFMCP_FORMS_DIR) and a Java runtime, JDK/JRE 25+ depending on channel. Live conversion of .fmb/.pll needs an Oracle Forms installation (ORACLE_HOME) with frmf2xml/frmcmp_batch; the Docker image works only in copy-mode with pre-converted files.
Installation
In SourceWeft
- Open Oracle Forms in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
Oracle Forms MCP
[CI] [CodeQL] [Release] [License] [Kotlin]
An MCP server that serves the content of Oracle Forms modules
(.fmb forms, .mmb menus, .pll PL/SQL libraries, .olb object libraries) found in a
directory, so AI assistants can inspect blocks, items, triggers, program units, and raw object
XML without opening Forms Builder.
Built as a Kotlin Multiplatform core (pure @Serializable models and ports) with a JVM MCP
server on top: declarative tool adapters over a single FormsService, stdio and HTTP transports,
and a fingerprint-based on-disk cache. Oracle tool conversion feeds a streaming StAX parser that
turns Forms XML into a structured index.
Why
Oracle Forms applications from the 1990s–2000s are still running critical business processes, but
their logic is locked inside binary .fmb/.pll modules that only Forms Builder can open. That
makes them opaque to modern AI tooling and painful to review, document, or migrate.
Oracle Forms MCP turns those modules into structured, queryable content so an AI assistant can:
- Understand a legacy app — enumerate blocks, items, triggers, and program units without a Forms IDE.
- Review & document PL/SQL — pull decoded trigger and program-unit bodies straight into the model's context.
- Assist modernization — feed decades-old business logic to an assistant for migration to APEX, Java, or a rewrite, and search across every module's source.
- Capture & retain knowledge — let the assistant record notes, tags, and cross-references on individual elements that persist across sessions and re-indexing, building up a durable map of a form no one fully remembers.
It is aimed at developers and teams doing Oracle Forms modernization, reverse engineering, code review, and documentation — anyone who needs to read Forms logic faster than opening it by hand.
See it work
A typical session against the bundled sample-forms directory:
How it works
list_modulesscans the configured--forms-dir(non-recursive) and reports each module's cache status:NOT_CACHED,CACHED,STALE(source changed on disk), orSOURCE_MISSING. A production forms directory holds thousands of modules, so the answer is filtered and paged: narrow withpattern/type/status, follownextCursorfor the rest, and readcountsByStatusfor the shape of the whole match.fetch_moduleproduces the module's text form in the cache and indexes it:ORACLE_HOMEset — binaries are converted with the Oracle tools in%ORACLE_HOME%\bin(then$FORMS_INSTANCE/bin, where Linux Forms 12c+ keepsfrmf2xml.sh):frmf2xmlfor.fmb/.mmb/.olb(XML),frmcmp_batch(Module_Type=LIBRARY Script=YES) for.pll(a.pldtext dump).ORACLE_HOMEnot set — pre-converted files are expected next to the modules (orders_fmb.xml,dupes_fmb.xml,picker_fmb.xml,toolbar_fmb.xml,mainmenu_mmb.xml,objects_olb.xml,utils.pld) and copied into the cache.
- A single StAX pass parses the XML into a structured index (blocks with items, triggers with
decoded PL/SQL, program units, LOVs, record groups, windows, canvases, …). PL/SQL bodies are
extracted to
.sqlsidecar files; every named XML element gets a line-range reference soget_object_xmlcan slice it back out of the converted file. - The other tools read the cached index. Caching is fingerprint-based (size + mtime + sha256 of
the source file): editing a module marks it
STALEand read tools ask for a re-fetch. annotate_elementandrelate_elementslet the assistant write durable meta-information back about individual elements (notes, tags, summaries, classifications, cross-references). This is kept in a separate store — not the derived index — so it survives re-fetching, and the read tools surface it inline. An annotation made before a source change is flagged, never dropped.
Tools
Every tool checks its arguments against its own input schema before running. An argument it does not have is never silently dropped: the call fails with every problem listed at once (unknown arguments with the name most likely meant, missing ones with their permitted values, values outside an enum), plus an example call where the tool has one.
Where an object's meaning lives
Two Forms properties carry more meaning than anything else in a module, and both used to be
reachable only one get_object_xml call at a time.
An item's property class is usually its role: Forms shops name their classes for what the
object does, so a push button is an LOV button and a text item is a filter field only because
of the class it inherits. get_block reports propertyClass on every item and block, confirmed
against the classes the module actually declares.
A window's modality decides how a form is read — a modal window is a dialog, so the code that
fills it and the code that consumes the result sit on opposite sides of one interaction.
get_module_overview(verbosity: "detailed") returns the window and canvas objects behind two of
its name lists: modality, size, toolbar canvases, and the window each canvas sits on.
A property Forms did not write comes back as null, meaning the object keeps the Forms default —
never false. On an item that has a property class it does not even mean the default: the class
supplies the value, and in a real form that is where most of an item's behaviour lives. So
get_block(verbosity: "detailed") returns both — items[].dml, what the item itself wrote, and
effectiveDml, the same properties with the class applied (item, then class, then whatever that
class is based on, followed into other modules that are already fetched). An item is listed in
effectiveDml only when that chain resolved to the end, so a null there really is the Forms
default. Every other item is a row of unresolvedItems, with the reason and — when fetching fixes
it — the module to fetch; propertyClasses and the result's hint give the same account per class
and name the fetch_module call. items: [...] narrows all of it to the items a question is about.
get_block(columns: true) adds the block's data-source columns, the columns no item supplies, and
which of those are mandatory in the database — the ones an insert fails on unless a trigger assigns
them.
A block's master-detail relations decide what it can be queried through, so get_block serves
them at every verbosity: block.relations, the relations it is the master of (join condition,
deferred, autoQuery, deleteRecord, preventMasterlessOperations), and detailOf, the
relations that name it as their detail, with the master each is written on. A detail with
preventMasterlessOperations cannot be queried except through its master — which the form enforces,
not the database, so a port has to enforce it itself.
Reading around a result
Everything indexed is a line range into a file the cache owns, and those ranges used to name a
path with no way to resolve it. Every result that points at a file now carries a source:
read_source takes either form back, so a search hit can be read in context and a truncated
fragment continued. The same files are readable as MCP resources —
oracleforms://{module}/converted for the converted text form and
oracleforms://{module}/plsql/{category}/{name} for one extracted block of PL/SQL — and every
such result also carries a resource_link content block, so a client can follow it without
parsing JSON. Reads are capped by lines and characters; a cut resource read says so in the text
it returns and names the call that continues it.
Paths stay cache-relative and URIs stay layout-independent: an absolute host path would mean nothing to a client talking to the container image or over HTTP.
Subclassed (inherited) objects
Forms lets a block, item, trigger or program unit be subclassed from another module, or copied
in with an object group (typically from an .olb). Either way the child stores only what it
overrides, so its PL/SQL body is genuinely empty in its own file while the code that runs lives in
the parent. Served naively that is not merely incomplete but wrong — an empty body reads as "this
button does nothing".
Every result that carries PL/SQL therefore also carries bodySource:
The inherited reference is stated in the parent's vocabulary and in the shape the tools take,
so it is directly callable — BAR_LIST.SELECT in this form is name: SELECT, ownerPath: BAR over
there:
For an object-group member, name is the object's own name and objectGroup names the group that
carried it — where the library files that group is not recorded in this module, so no ownerPath
is claimed for it. A parent in the same module is a property class: it supplies properties
rather than a definition, hides nothing, and is deliberately not reported as inheritance.
get_trigger and get_program_unit take resolve: true to follow the pointer in one call. It
reads only modules that are already cached — reaching an un-cached one would mean converting
it, which a read-only tool must not do — and falls back to the pointer plus the fetch_module
hint rather than failing. get_block flags subclassed blocks and items the same way, and
get_object_xml returns the pointer resolved to the level it was asked about (Forms writes the
raw ParentModule attribute on the enclosing owner, so the fragment alone cannot answer it).
Tracing across modules
A question about one form rarely stays inside it. A modal window is opened by whoever calls it, the
value it hands back travels through a :GLOBAL, and its toolbar is defined in a third form
entirely — so search_source, which searches the module you name, cannot answer any of the three:
Each hit names its module (moduleSpec, the NAME.ext string the other tools take), the
cache-relative file, the line, a snippet, and the uri that opens it. Matching is
case-insensitive by default, because Forms code writes the same module name as PICKER, picker
and Call_Form('picker') in the same code base; pass ignoreCase: false when precision matters.
Only modules that have been fetched are searched — reaching an un-fetched one would mean converting it, which a read-only tool must not do. So the coverage is part of the answer rather than an assumption:
To make a whole directory searchable, warm the cache before the session instead of one
fetch_module at a time: --prefetch runs the server once as a batch job that converts and
indexes every matching module, one line per module on stderr, then exits (with 1 if any failed).
Modules already current cost a fingerprint check, so a rerun picks up where a stopped one left off.
A module is current as long as its file is unchanged — which says nothing about whether it was
converted right. frmf2xml run without the forms directory on FORMS_PATH, for one, succeeds but
leaves out every inherited object, and that result is served until the module is saved again. After
fixing the conversion setup, convert again with --force (--prefetch-all --force), or one module
at a time with fetch_module(module=…, force=true). Annotations are kept either way.
Both bounds are enforced and resumable: maxResults hits per page, and a ceiling on how many
modules one call reads — a query that matches nothing would otherwise read every converted file in
the cache before answering. Either one sets truncated and returns a nextCursor to pass back
with the same arguments. The cursor is bound to those arguments, so it cannot silently continue a
different search.
Annotations
Meta-information the assistant records back about an element rather than reads from it —
semantic notes, tags, classifications, and cross-reference relations. It is persisted in a durable
store, kept separate from the derived index (not in the protocol _meta field), so it survives
fetch_module re-indexing and is served back to later sessions. Each entry carries its author and
is flagged staleAgainstSource when it predates the module's current source, so a note is never
silently dropped. The read tools above (get_module_overview, get_block, get_trigger,
get_program_unit, get_object_xml) surface an element's annotations inline.
Plus a resource per cached module (oracleforms://ORDERS.fmb/index), the
oracleforms://{module}/index and oracleforms://{module}/annotations resource templates, and an
explain_module prompt.
Quick start
Pick the channel that matches your client: the plugin for Claude
Code, the .mcpb bundle for Claude Desktop, the
Docker image for everything else, or a
build from source. The server is published to the
official MCP Registry as
io.github.aoreshkov/oracle-forms-mcp.
Claude Code (one-command plugin)
Claude Code prompts for your forms directory, then fetches and checksum-verifies the released
server into the plugin's data directory on first use — no clone, no build, no JSON to edit. The
tools are live after /reload-plugins, and the plugin's trace-form skill teaches the traversal
above — the order to ask in, and the readings that mislead — so a trace does not start with grep.
Skip the prompt and the server still starts; its tools then answer with an error asking for the
directory until you set it with /plugin configure oracle-forms@oracle-forms-mcp.
Requires a JDK 25+ (
javac -version), because the plugin's bootstrap runs in Java's single-file source mode. It uses the one on yourPATHunless you set the plugin'sjavaoption to another JDK'sjavaexecutable, and falls back toJAVA_HOMEwhen that one is too old for the server. With only a JRE, use the.mcpbbundle orclaude mcp addbelow.
Details, configuration options, and the escape hatches are in the plugin README.
Build from source
Requires a JDK 25+ to build (Gradle provisions one if missing) and a JRE 25+ to run. Build and install:
Register with Claude Code (stdio):
The server script runs on the Java installation OFMCP_JAVA_HOME names, else JAVA_HOME, else
the java on PATH, so a JRE that is not on PATH is one variable away:
Claude Desktop (one-click bundle)
Download oracle-forms-mcp-<version>.mcpb from the
latest release and open it. Claude
Desktop installs it as a connector and prompts for your forms directory with a native folder picker
— no JSON editing, no local build. Leave it empty and the connector still starts; its tools then
answer with an error asking for the directory until you set it in the connector's settings.
Requires a JRE 25+. The bundle ships the server, not a Java runtime. It runs on the Java installation chosen in the connector's settings, else the one
JAVA_HOMEnames, else thejavaon yourPATH. The MCPB manifest format can only declare Node and Python runtimes, so this cannot be checked at install time: if the connector fails to start, choose the Java installation in its settings, or confirmjava -versionworks in a terminal.
The same copy-mode caveat as Docker applies unless the machine has an
Oracle Forms installation (ORACLE_HOME) for live .fmb/.pll conversion.
Other MCP clients (Cursor, VS Code, Claude Desktop without the bundle) — via the Docker image
The published image runs the server over stdio with no local build. Point the volume mount at
your forms directory (copy-mode: the pre-converted *_fmb.xml/*.pld files must sit next to the
modules — see Docker).
Claude Desktop (claude_desktop_config.json) and Cursor (~/.cursor/mcp.json) use the same shape:
VS Code (.vscode/mcp.json) uses a servers key instead:
The ofmcp-cache named volume keeps the parsed-module cache and — more importantly — the durable
annotation store across container restarts; --rm removes the container but not a named volume. Drop
it and the notes/tags/relations the assistant records won't survive the next run. See
Docker for the bind-mount variant and its one-time chown.
Prefer the native launcher? Swap "command": "docker", "args": [...] for
"command": "/abs/path/to/server/build/install/server/bin/server", "args": ["--forms-dir", "/abs/path/to/forms"].
Try it without any Oracle installation using the bundled fixtures:
HTTP transport:
The HTTP transport listens on 127.0.0.1 only. It has no authentication of its own, so the bind
address decides who can call every tool, including fetch_module, which runs your converter, and
the annotation writes. --host (or OFMCP_HTTP_HOST) binds it elsewhere, and the server logs a
warning at startup whenever that address is not loopback. The Host/Origin check stops a browser
from being used against the server through DNS rebinding. It does not stop a client on the network
that sends Host: 127.0.0.1 itself.
Sessions expire after 30 minutes without a request (an open event stream does not count, a tool
call still running does), and their id then answers 404, on which an MCP client starts a new
session. At most 10,000 sessions are open at once.
Docker (copy-mode only)
A container image is published to ghcr.io/aoreshkov/oracle-forms-mcp. Oracle's frmf2xml /
frmcmp_batch binaries are proprietary and not bundled, so the image works only in
copy-mode: the modules you mount must already have their pre-converted text form
(*_fmb.xml/*_mmb.xml/*_olb.xml/*.pld) sitting next to them. For live .fmb/.pll
conversion, run the server on a host with an Oracle Forms installation (ORACLE_HOME set).
Persisting the cache and annotations. Without a volume, the cache and the durable annotation
store live in the container's writable layer and are discarded when it exits. Mount a volume at
/home/mcp/.cache to keep them across runs:
A named or anonymous volume inherits the image's non-root ownership (uid 10001) and just works.
A host bind mount does not — Docker never chowns the target — so run chown 10001 /host/cache
once on the host first, or redirect the writes with --cache-dir / --annotations-dir onto a path
the container user can write.
Running the HTTP transport in a container. Inside the container, loopback is the container's
own, so the server has to listen on 0.0.0.0 there. Publish the port on the host's loopback so
that it stays unreachable from the network:
A plain -p 3000:3000 publishes the port on every interface of the host, which exposes every
tool to anyone who can reach it.
Options
server --help is the complete reference: every option with its default and its variable, and
the rules between them. --port, --allowed-host and --allowed-origin need --transport http,
and --prefetch and --prefetch-all cannot be combined. Exit status 0 is success, 1 a prefetch
in which a module failed, 2 a command-line mistake, and 70 an unexpected failure, whose stack trace
precedes it on stderr.
Both allowlists add to localhost rather than replace it, and the two are checked separately:
allowing a host does not allow an origin. A browser-based client opened from anywhere but
localhost needs its origin passed with --allowed-origin, or it gets 403. Clients that are not
browsers send no Origin and need only the host.
The forms directory, the converter options and the HTTP bind address can also be set as
environment variables, for clients that configure a server with variables rather than arguments
(docker run -e, the env block of an MCP config, a plugin host that starts every installation
with the same command line). The command line always wins over its variable — for the forms
directory, in either spelling:
--convert-command and --converted-dir are also exposed as configuration in the
Claude Code plugin, and all three in
the .mcpb bundle (Claude Desktop's connector settings) and the registry listing for the Docker
image — so whichever channel you install from, you can point the server at your own converter and
your own output directory without editing JSON by hand. Leaving any of them unset is always valid:
an empty value counts as "not configured".
Keeping the converted XML
By default a module's converted text form lives inside its cache entry. Point --converted-dir at
a directory of your own to keep the XML where you can read, diff, or feed it to other tooling:
All modules share that one flat directory, each file named the way Oracle names it —
orders_fmb.xml, mainmenu_mmb.xml, utils.pld — so a re-fetch replaces a module's file rather
than accumulating copies. The directory is created if missing, and it must not be the forms
directory itself (the names would collide with the pre-converted modules read from there); a
subdirectory of the forms directory is fine, since it is scanned non-recursively.
This is the directory the converter writes into, not somewhere files are moved to afterwards:
the converter runs with it as its working directory, so a wrapper that writes to a fixed location
can simply be pointed at it. Output is attributed to a module by Oracle's naming first
(orders_fmb.xml), falling back to "the newest matching file this run wrote" for converters that
name their output freely. A file that was already there when the run started — another module's
text form, a killed run's partial output — never counts, and conversions sharing the directory are
serialised, rename included, so two modules cannot be mixed up. Whatever name the converter chose, the file is renamed to the
canonical one, and the index records it by a stable converted/<name> path either way — so an
index stays valid whether or not this option is set.
Using your own converter
If your site wraps the Forms tools — its own environment setup, logon handling, a patched
frmf2xml, or a different converter entirely — point the server at it:
The value is a whole command line, not just an executable, so a wrapper that needs arguments of its own — an interpreter, a container, a compatibility layer — is configured directly:
Two syntaxes are accepted, and neither is ever handed to a shell — the command is split here and spawned with an argv list:
- A quoted string. Split on whitespace, with
"…"or'…'grouping a part that contains spaces. Backslashes are literal, so Windows paths need no doubling. - A JSON array —
["wine", "f2x.exe", "-xml"]— one element per argument, the same shape MCP clients use forcommand/args. Prefer it when quoting gets awkward, and note that JSON itself requires\\for a backslash.
A value that names an existing file is taken whole, spaces and all, so a plain path configured before this option accepted arguments keeps working unquoted.
The module's absolute path goes wherever you write {}; with no {} in the command it is
appended as the last argument, which is what the earlier <command> <module> convention did. The
command runs with the working directory set to the converted directory — that module's cache
directory by default — and is expected to write the text form there. A tool that cannot be pointed
at a working directory is given the output file instead: {out} is replaced with its absolute
path (<converted dir>/orders_fmb.xml, <converted dir>/utils.pld). {out} is only ever
substituted, never appended, so a command without it is run exactly as before. This mirrors how frmf2xml is
driven, so a script that already wraps it needs no changes. Emit the same formats the parser reads:
XML for .fmb/.mmb/.olb, a .pld dump for .pll. Oracle's <name>_fmb.xml naming is
preferred but not required — any .xml (or .pld for a library) written into the working directory
is picked up.
The program itself may be a path (absolute or relative to where the server was started) or a bare
name, which is looked up on PATH. An argument, though, is read by the command, which runs in
the converted directory — so a file shipped beside the server and passed to an interpreter would be
looked for there. Write it with {start}, which is replaced with the absolute directory the
server was started in (and, like {out}, never appended):
That is how a plugin whose host starts its server in the plugin's own directory, and passes configuration through literally, names a converter it carries.
With --converted-dir that working directory is your directory, so
a wrapper that writes to a fixed place of its own — rather than to wherever it was started — works
by pointing --converted-dir at it. Scripts that write into their working directory need no
adjustment either way.
Precedence is --convert-command → ORACLE_HOME → copy-mode, so an explicitly configured
command wins even on a machine with a Forms installation (.pll libraries can be taken out of
that order — see below). A blank value counts
as unset. Like ORACLE_HOME, the command is parsed and validated at the first conversion rather
than at startup, so a stale setting still leaves cached modules readable; the error then names the
flag to fix.
The output must be freshly written. Because Forms-era tools return unreliable exit codes, a run is judged by its output file, and a file older than the run is treated as a leftover from a previous failed attempt. A script that copies a pre-generated file with
copy,xcopy, orcp -ppreserves the source's timestamp and will be rejected with "produced no output file". Redirect or rewrite instead (type src > out,cat src > out), ortouchthe result.
The command is operator configuration only — no tool argument can choose or extend it. Tool callers supply a module name, which is resolved against the scanned forms directory before the converter sees it, and the command is spawned directly with an argv list rather than through a shell, so nothing in a module's path or in your own quoting can turn into a second command.
Converting PL/SQL libraries with their own command
Since 0.11.0. No Oracle tool converts a .pll to XML: frmf2xml accepts forms, menus, and object
libraries only, and a library is dumped to .pld text by frmcmp instead. A --convert-command
built around frmf2xml therefore fails on every library. --compile-command gives .pll modules
a command line of their own, with the same syntax and placeholders, and leaves every other module
type where it was:
or, as a JSON array:
Write it on one line. The value is split into arguments without a shell, so a \ line
continuation copied into a configuration field would reach the tool as a literal argument.
Give frmcmp Output_File={out}. Without it, frmcmp writes the .pld next to the module —
into the forms directory, whatever its working directory — where the server would then read it as
a pre-converted file. When a run produces nothing in the converted directory but did leave a fresh
file next to the module, the error names that file and says to add {out}; the server does not
delete it.
Logon=NO matters too: without it (or a real Userid=), frmcmp prints its usage and exits 0
having written nothing.
Which converter a module reaches:
With --compile-command unset, nothing changes: a --convert-command that already handles
libraries keeps receiving them. Where a type is served in copy-mode, its pre-converted text form is
what the cache entry is checked against, so re-exporting an _fmb.xml still marks that form stale
while its libraries are converted from their binaries.
Cache
%LOCALAPPDATA%\oracle-forms-mcp (Windows), ~/Library/Caches/oracle-forms-mcp (macOS),
$XDG_CACHE_HOME/oracle-forms-mcp (Linux). One directory per module:
Safe to delete at any time; modules are simply re-fetched. With
--converted-dir the converted/ file is written to the directory
you name instead and the rest of the entry stays here.
Annotations are not part of this derived cache. They live in a separate annotations/ store
(one NAME.ext.json per module, defaulting to <cache dir>/annotations, overridable with
--annotations-dir), so deleting a module's cache entry — or re-fetching it — leaves the notes,
tags, and relations you recorded intact.
Notes on the Oracle tools
- The tools are looked for in
$ORACLE_HOME/bin, then in$FORMS_INSTANCE/bin: on Linux, Forms 12c and later shipfrmf2xmlonly asfrmf2xml.shin the Forms instance..shwrappers are accepted there (and.bat/.cmd/.exeon Windows). frmf2xmlwrites its output into the process working directory on some installs and next to its input on others (Linux 14.1.2). The server therefore converts a copy of the module in a private staging directory, with that directory as cwd, and moves the result into the converted directory (--converted-dir, else the module's cache dir) — so the forms directory is never written to and may be read-only. It passesOVERWRITE=YES USE_PROPERTY_IDS=NO.- Outside Windows, the tools run with
TERM=vt220andFORMS_API_TK_BYPASS=TRUEunless the server's environment already sets them: an MCP host may start the server with neither a terminal nor a display, and without themfrmcmp_batchfails withFRM-91500andfrmf2xmlwith "Failed to create new forms context".TERM=dumbcounts as unset, sincefrmcmp_batchfails under it the same way. Set either yourself to override it. frmcmp_batchis preferred overfrmcmp(headless); the server passesScript=YES Batch=YES Logon=NOand an explicitOutput_File— without one,frmcmpwrites the.pldnext to the module rather than into its working directory — and augmentsFORMS_PATHwith the forms dir so attached libraries resolve.- Forms tools have unreliable exit codes — success is judged by the output file existing, being non-empty, and being newer than the invocation; failures surface the tool's output tail.
.pldfiles may be written in the client NLS charset; the parser reads UTF-8 with a windows-1252 fallback (setNLS_LANGaccordingly if you see mojibake).
Privacy Policy
Oracle Forms MCP runs entirely on your machine and collects nothing.
- Data collection. None. The server has no telemetry, analytics, crash reporting, or accounts, and it makes no network requests of its own.
- What it reads and stores. It reads the modules in the
--forms-diryou name and writes derived files — converted text forms, decoded PL/SQL, andindex.json— to the local cache (or--converted-dir). Annotations you or your assistant record are written to the local annotations store. All of these stay on your disk, and you can delete them at any time. - Where content goes. Module content leaves the server only as responses to the MCP client you
connected it to. What that client does with it — including sending it to a model provider — is
governed by that client's own privacy policy, not this one. The
httptransport listens on127.0.0.1only unless you bind it elsewhere with--host. - Third-party sharing. None. The one exception to "no network" is the Claude Code plugin's
launcher: it asks the GitHub API for the latest release at most once a day and downloads the
release zip and its checksum from GitHub Releases. Those requests go to GitHub, under
GitHub's privacy statement,
and carry nothing from your modules. Pin
server_versionor useOFMCP_SERVER_HOMEto avoid them. - Retention. Cached and annotation files persist until you delete them; the software keeps nothing anywhere else.
- Contact. Questions go to GitHub issues; security reports follow SECURITY.md.
Development
Converter behavior is tested against a fake ORACLE_HOME (stub scripts); the full copy-mode
pipeline is covered end-to-end by FormsServiceIntegrationTest against the bundled fixtures.
Source: README.md at commit e560187
Tools
0Version history
10- v0.20.0LatestNov 7, 2026
- v0.18.0Nov 5, 2026
- v0.17.0Oct 29, 2026
- v0.15.0Oct 19, 2026
- v0.14.0Oct 16, 2026
- v0.13.1Oct 13, 2026
- v0.13.0Sep 18, 2026
- v0.12.0Sep 18, 2026
- v0.11.0Sep 16, 2026
- v0.10.0Sep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts