Upload or replace OPA policy
opa_put_policyUpload or replace a Rego policy in OPA by specifying its ID and source text. Policy is sent as plain text and parsed server-side.
Instructions
Upload a Rego policy under the given ID. Replaces any existing policy with that ID. The policy is uploaded as raw text/plain — OPA parses it on the server side.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Policy ID to create or replace. | |
| source | Yes | Rego source. |
Implementation Reference
- The handler function for 'opa_put_policy'. It accepts id and source (Rego source) as input, sends a PUT request to the OPA server's /v1/policies/{id} endpoint with the source as text/plain, and returns a success envelope with { id, replaced: true } or maps the OPA client error on failure.
server.registerTool( 'opa_put_policy', { title: 'Upload or replace OPA policy', description: 'Upload a Rego policy under the given ID. Replaces any existing policy with that ID. The policy is uploaded as raw text/plain — OPA parses it on the server side.', inputSchema: { id: z.string().min(1).describe('Policy ID to create or replace.'), source: z.string().min(1).describe('Rego source.'), }, }, async ({ id, source }) => { return withToolEnvelope<{ id: string; replaced: boolean }>(config, async () => { try { await opa.request({ method: 'PUT', path: `/v1/policies/${encodeURIComponent(id)}`, rawBody: source, rawContentType: 'text/plain', }); return ok({ id, replaced: true }); } catch (e) { return mapOpaClientError(e); } }); }, ); - Zod input schema for the tool: requires id (non-empty string) and source (non-empty string for Rego source code).
inputSchema: { id: z.string().min(1).describe('Policy ID to create or replace.'), source: z.string().min(1).describe('Rego source.'), }, - src/tools/server-management/index.ts:11-21 (registration)Registration chain: server.ts calls registerTools() -> registerServerManagementTools() -> registerPolicyTools() which registers 'opa_put_policy' via server.registerTool().
import { registerDataTools } from './data.js'; import { registerDecisionTools } from './decisions.js'; import { registerPolicyTools } from './policies.js'; import { registerStatusTools } from './status.js'; export function registerServerManagementTools(server: McpServer, config: Config): void { registerPolicyTools(server, config); registerDataTools(server, config); registerDecisionTools(server, config); registerStatusTools(server, config); } - src/tools/index.ts:35-43 (registration)Top-level registration function that delegates to category-specific registrars including registerServerManagementTools.
import { registerServerManagementTools } from './server-management/index.js'; export function registerTools(server: McpServer, config: Config): void { registerAuthoringTools(server, config); registerEvaluationTools(server, config); registerBundleTools(server, config); registerServerManagementTools(server, config); registerHelperTools(server, config); } - src/lib/opa-client.ts:62-123 (helper)The OpaClient.request() method used by the handler to make the PUT request. It handles rawBody (text/plain) for policy upload, authentication via Bearer token, and error mapping for unreachable/auth/HTTP errors.
async request<T = unknown>(opts: RequestOptions): Promise<T> { const url = this.buildUrl(opts.path, opts.query); const headers: Record<string, string> = { Accept: 'application/json', ...(opts.headers ?? {}), }; if (this.config.opaToken) { headers['Authorization'] = `Bearer ${this.config.opaToken}`; } let bodyToSend: string | undefined; if (opts.rawBody !== undefined) { if (opts.body !== undefined) { throw new Error('OpaClient.request: pass either `body` or `rawBody`, not both.'); } bodyToSend = opts.rawBody; if (!headers['Content-Type']) { headers['Content-Type'] = opts.rawContentType ?? 'text/plain'; } } else if (opts.body !== undefined) { bodyToSend = JSON.stringify(opts.body); if (!headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.config.httpTimeoutMs); const init: RequestInit = { method: opts.method, headers, signal: controller.signal, }; if (bodyToSend !== undefined) { init.body = bodyToSend; } let response: Response; try { response = await fetch(url, init); } catch (e) { throw new OpaUnreachableError(this.config.opaUrl, e); } finally { clearTimeout(timer); } if (response.status === 401) { throw new OpaAuthError(); } const contentType = response.headers.get('content-type') ?? ''; const isJson = contentType.includes('application/json'); const payload: unknown = isJson ? await response.json() : await response.text(); if (!response.ok) { throw new OpaHttpError(response.status, payload); } return payload as T; }