@ratifia/mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/index.js +267 -0
  4. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ratifia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # ratifia-mcp
2
+
3
+ Human-in-the-loop for AI agents. This [MCP](https://modelcontextprotocol.io) server lets an agent (Claude Code, Claude Desktop, etc.) **pause on a human decision mid-task and resume once it's answered** — the agent is the "engine", [Ratifia](https://ratifia.com) owns the decision, notification, and verdict.
4
+
5
+ ## Install
6
+
7
+ Published on npm as [`@ratifia/mcp`](https://www.npmjs.com/package/@ratifia/mcp).
8
+ Add it to your MCP client — `npx` fetches and runs it:
9
+
10
+ ```json
11
+ {
12
+ "mcpServers": {
13
+ "ratifia": {
14
+ "command": "npx",
15
+ "args": ["-y", "@ratifia/mcp"],
16
+ "env": {
17
+ "RATIFIA_API_KEY": "your-org-worker-api-key",
18
+ "RATIFIA_APPROVER_EMAIL": "you@example.com"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ Point separate registrations at different environments by giving each its own `RATIFIA_API_URL` + key (e.g. `ratifia-stg` and `ratifia-prod`).
26
+
27
+ ### Local clone (development)
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "ratifia": {
33
+ "command": "node",
34
+ "args": ["/absolute/path/to/ratifia-mcp/dist/index.js"],
35
+ "env": {
36
+ "RATIFIA_API_KEY": "your-org-worker-api-key",
37
+ "RATIFIA_APPROVER_EMAIL": "you@example.com"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Clone, then `npm install && npm run build` (rebuild after source changes). Use
45
+ `npx tsx src/index.ts` instead of the built file to skip the build step while iterating.
46
+
47
+ ## Configuration (env)
48
+
49
+ | Var | Required | Default | Purpose |
50
+ |-----|----------|---------|---------|
51
+ | `RATIFIA_API_KEY` | ✅ | — | Org-scoped worker API key. |
52
+ | `RATIFIA_API_URL` | | `https://api.ratifia.com` | Ratifia API base. |
53
+ | `RATIFIA_APP_URL` | | `https://app.ratifia.com` | Dashboard base for decision links. |
54
+ | `RATIFIA_APPROVER_EMAIL` | | — | Who the decision is assigned to / notified. |
55
+ | `RATIFIA_POLICY` | | — | Named policy whose route decides **how** you're notified (e.g. to your phone). |
56
+
57
+ Routing (email / SMS / escalation) is decided **inside Ratifia** by your org's policy — this server is a thin request/verdict bridge.
58
+
59
+ ## Tools
60
+
61
+ | Tool | What it does |
62
+ |------|--------------|
63
+ | `request_decision` | Create a decision, notify the human, return a `decision_id`. |
64
+ | `check_decision` | Poll a decision's status once. |
65
+ | `await_decision` | Block until the verdict lands (or a max wait elapses). |
66
+ | `list_pending_decisions` | The agent's inbox — what's outstanding. |
67
+ | `cancel_decision` | Withdraw a decision the agent no longer needs. |
68
+
69
+ ## Decision shapes
70
+
71
+ `request_decision` isn't only yes/no. Shape the answer with `response_type`:
72
+
73
+ | `response_type` | The human… | You read the answer from |
74
+ |-----------------|-----------|--------------------------|
75
+ | `approval` (default) | approves or rejects | `verdict` (+ `note`) |
76
+ | `select` | picks from `options` you provide (set `allow_multiple` for many) | `response_value.selected` (option ids → labels via `response_spec.options`) |
77
+ | `text` | types a value | `response_value.text` |
78
+
79
+ ```
80
+ request_decision(
81
+ title: "Which environment?",
82
+ question: "Where should I deploy?",
83
+ response_type: "select",
84
+ options: [{ label: "Staging" }, { label: "Production" }]
85
+ )
86
+ ```
87
+
88
+ ### Conversational Approvals — propose a draft, get it refined
89
+
90
+ Pass `proposed_output` to attach an **editable draft** (e.g. an email you want to
91
+ send). The human refines it in conversation — in the dashboard or by replying in the
92
+ Slack thread — and you get **their** final version back:
93
+
94
+ ```
95
+ request_decision(
96
+ title: "Send welcome email",
97
+ question: "Review this before I send it.",
98
+ proposed_output: { title: "Welcome email", content: "<your draft>" }
99
+ )
100
+ ```
101
+
102
+ When it resolves, `await_decision` / `check_decision` return the final text in
103
+ `output.content` (with a `refined` flag telling you whether the human changed it).
104
+ **Act on `output.content`, not the draft you sent** — it may differ.
105
+
106
+ ### Gate a specific tool call — show what you'd run
107
+
108
+ When the decision gates a concrete tool/function call, pass `tool_name` (and
109
+ `tool_args`). The reviewer sees a structured **Tool call** panel — the exact
110
+ action and arguments — instead of it being buried in free-text `context`:
111
+
112
+ ```
113
+ request_decision(
114
+ title: "Approve refund?",
115
+ question: "This exceeds the $5,000 auto-approve floor.",
116
+ tool_name: "issue_refund",
117
+ tool_args: { amount_cents: 600000, currency: "USD", claim_id: "ACM-2024-00003" }
118
+ )
119
+ ```
120
+
121
+ `tool_args` is optional (some tools take none); it's ignored unless `tool_name`
122
+ is set.
123
+
124
+ ## How it works
125
+
126
+ 1. The agent calls `request_decision` before a consequential/irreversible step (deploy, delete, spend, send externally), an ambiguous call it shouldn't guess, or to get a human to pick/refine something.
127
+ 2. Ratifia notifies the human (even away from keyboard) per the org's routing.
128
+ 3. The agent `await_decision`s (or polls `check_decision`) — read `verdict`/`note`, plus `response_value` (select/text) or `output` (a refined draft).
129
+ 4. The agent proceeds only on `approved`; on `rejected`/`expired` it adapts.
130
+
131
+ Learn more at [ratifia.com](https://ratifia.com).
package/dist/index.js ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ratifia MCP server (HITL for AI agents).
4
+ *
5
+ * Exposes tools so an agent (e.g. Claude Code) can park on a human decision
6
+ * mid-task and resume once it's answered — the agent is the "engine", Ratifia
7
+ * owns the decision + notification + verdict:
8
+ * - request_decision : create a Ratifia decision, notify the human (per the
9
+ * org's routing), return a decision_id.
10
+ * - check_decision : poll the verdict once (pending → approved/rejected + note).
11
+ * - await_decision : block until the verdict lands (or a max wait elapses),
12
+ * so the agent doesn't have to poll in a loop.
13
+ * - list_pending_decisions : the agent's own inbox — what's outstanding.
14
+ * - cancel_decision : withdraw a decision the agent no longer needs.
15
+ *
16
+ * Routing (HOW a human is reached — email/SMS/phone/escalation) is decided
17
+ * INSIDE Ratifia by the org's policy/route, never by this client. The MCP is a
18
+ * thin request/verdict bridge.
19
+ *
20
+ * Config via env (so `ratifia-prod` and `ratifia-stg` are just two registrations
21
+ * pointing at different API URLs / keys):
22
+ * RATIFIA_API_URL API base (default https://api.ratifia.com)
23
+ * RATIFIA_API_KEY org-scoped worker API key (required)
24
+ * RATIFIA_APP_URL dashboard base for the decision link (default https://app.ratifia.com)
25
+ * RATIFIA_APPROVER_EMAIL who the decision is assigned to / notified
26
+ * RATIFIA_POLICY optional: policy name whose route decides HOW you're notified
27
+ * (e.g. a route to your phone)
28
+ */
29
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
30
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
31
+ import { z } from 'zod';
32
+ const API_URL = (process.env.RATIFIA_API_URL ?? 'https://api.ratifia.com').replace(/\/+$/, '');
33
+ const API_KEY = process.env.RATIFIA_API_KEY;
34
+ const APP_URL = (process.env.RATIFIA_APP_URL ?? 'https://app.ratifia.com').replace(/\/+$/, '');
35
+ const APPROVER_EMAIL = process.env.RATIFIA_APPROVER_EMAIL;
36
+ const POLICY = process.env.RATIFIA_POLICY;
37
+ if (!API_KEY) {
38
+ console.error('RATIFIA_API_KEY is required');
39
+ process.exit(1);
40
+ }
41
+ async function api(path, init) {
42
+ const res = await fetch(`${API_URL}${path}`, {
43
+ ...init,
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ Authorization: `Bearer ${API_KEY}`,
47
+ ...(init?.headers ?? {}),
48
+ },
49
+ });
50
+ const text = await res.text();
51
+ if (!res.ok)
52
+ throw new Error(`Ratifia ${path} → ${res.status}: ${text.slice(0, 300)}`);
53
+ return (text ? JSON.parse(text) : {});
54
+ }
55
+ const getDecision = (id) => api(`/v1/worker/decisions/${encodeURIComponent(id)}`, { method: 'GET' });
56
+ /** Agent-facing shape for a decision's current state. */
57
+ const formatDecision = (d) => ({
58
+ decision_id: d.id,
59
+ status: d.status, // PENDING | APPROVED | REJECTED | EXPIRED
60
+ verdict: d.decision, // approve | reject | null
61
+ note: d.note, // the human's free-text input/answer — read and follow it
62
+ // FN-181: for a select/text decision the real answer is here, not in `note`.
63
+ // `response_value.selected` holds the chosen option id(s); map them back to
64
+ // labels via `response_spec.options`. `response_value.text` holds a typed value.
65
+ ...(d.responseSpec ? { response_spec: d.responseSpec } : {}),
66
+ ...(d.responseValue ? { response_value: d.responseValue } : {}),
67
+ // FN-185: if you sent a proposed_output (e.g. a draft email), the human may
68
+ // have refined it (in the dashboard or a Slack thread). `output.content` is the
69
+ // effective final text to act on — use it over your original draft.
70
+ ...(d.output ? { output: d.output, refined: Boolean(d.refinedOutput) } : {}),
71
+ decided_by: d.decidedBy,
72
+ decided_at: d.decidedAt,
73
+ });
74
+ const asToolResult = (payload) => ({
75
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
76
+ });
77
+ function buildResponseSpec(responseType, options, allowMultiple) {
78
+ if (!responseType || responseType === 'approval')
79
+ return undefined;
80
+ if (responseType === 'text')
81
+ return { type: 'text' };
82
+ // select
83
+ const seen = new Set();
84
+ const specOptions = (options ?? []).map((o, i) => {
85
+ // Cap at 40: option ids ride in a Discord button custom_id (100-char limit)
86
+ // alongside the 28-char decision id, so keep them short.
87
+ let id = (o.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `opt-${i}`).slice(0, 40);
88
+ while (seen.has(id))
89
+ id = `${id}-${i}`;
90
+ seen.add(id);
91
+ return { id, label: o.label, ...(o.description ? { description: o.description } : {}) };
92
+ });
93
+ return {
94
+ type: 'select',
95
+ options: specOptions,
96
+ min: 1,
97
+ max: allowMultiple ? specOptions.length : 1,
98
+ };
99
+ }
100
+ const server = new McpServer({ name: 'ratifia', version: '0.4.0' }, {
101
+ instructions: [
102
+ 'Ratifia lets you request a HUMAN decision or input mid-task (human-in-the-loop) and resume once it is answered.',
103
+ '',
104
+ 'When to use request_decision: before a consequential or irreversible action (prod deploy, delete/drop, data migration, spending, sending something externally, credential rotation), or when a requirement is ambiguous and guessing wrong would be costly. Skip it for trivial, reversible, or clearly-authorized steps — that is just friction.',
105
+ '',
106
+ 'Workflow:',
107
+ '1. Call request_decision with a short title, a precise question, and enough context to decide (the command/diff/options + tradeoffs). Set timeout_minutes if it is time-sensitive.',
108
+ '2. It returns a decision_id (+ url). The human is notified and may be away from their keyboard, so a verdict can take a while. (If the org policy auto-resolved that no approval was needed, it returns approval_required=false — just proceed.)',
109
+ '3. Wait for the verdict: prefer await_decision (it blocks until answered) over polling check_decision in a loop. Either way, "approved"/"rejected" comes with a note — the note is the human\'s actual answer/input, so read and follow it. If you asked for a choice/typed value (response_type select/text), the answer is in response_value (selected option ids — map them back via response_spec.options — or text), not just the note.',
110
+ '4. Do NOT perform the gated action until it resolves approved. On rejected or expired, do not proceed — adapt or ask again.',
111
+ 'Refining content: to have a human review/edit a draft (e.g. an email you want to send), pass proposed_output.content. They can refine it (in the dashboard or by replying in the Slack thread); once approved, use output.content as the final text — it may differ from what you sent.',
112
+ 'Prefer one well-scoped decision with rich context over many small ones.',
113
+ ].join('\n'),
114
+ });
115
+ server.tool('request_decision', 'Ask the human for a decision or input before proceeding (human-in-the-loop). Creates a Ratifia decision, notifies them (even away from keyboard), and returns a decision_id to await/poll. Use whenever you need approval, a choice, or input you should not assume. Set response_type to shape the answer: "approval" (yes/no, default), "select" (pick from options you provide), or "text" (a typed value) — for select/text the answer comes back in response_value. To have a human refine a draft (e.g. an email), pass proposed_output — the approved/edited text comes back in output.content. If this decision gates a specific tool/function call, pass tool_name (and tool_args) so the reviewer sees a structured "Tool call" panel of exactly what you would run.', {
116
+ title: z.string().describe('Short title, e.g. "Deploy API to prod?"'),
117
+ question: z.string().describe('Exactly what you need the human to decide or provide.'),
118
+ context: z
119
+ .string()
120
+ .optional()
121
+ .describe('Details to help them decide: options, tradeoffs, a diff, the command you would run.'),
122
+ timeout_minutes: z
123
+ .number()
124
+ .int()
125
+ .positive()
126
+ .optional()
127
+ .describe('Auto-expire (and reject) if unanswered after N minutes. Omit to wait indefinitely.'),
128
+ response_type: z
129
+ .enum(['approval', 'select', 'text'])
130
+ .optional()
131
+ .describe("What kind of answer you need. 'approval' (default) = yes/no gate. 'select' = the human picks from `options`. 'text' = the human types a value. For select/text, read the answer from `response_value` (not just `note`)."),
132
+ options: z
133
+ .array(z.object({
134
+ label: z.string().describe('What the human sees for this choice.'),
135
+ description: z.string().optional().describe('Optional one-line clarification of the choice.'),
136
+ }))
137
+ .min(2)
138
+ .optional()
139
+ .describe("The choices for a 'select' decision (at least 2). Required when response_type is 'select'."),
140
+ allow_multiple: z
141
+ .boolean()
142
+ .optional()
143
+ .describe("For 'select': allow picking more than one option (default single-choice)."),
144
+ proposed_output: z
145
+ .object({
146
+ title: z.string().optional().describe('Short label for the artifact, e.g. "Email body".'),
147
+ content: z.string().describe('The draft the human can edit before approving (e.g. the email text).'),
148
+ })
149
+ .optional()
150
+ .describe('A draft artifact the human can refine before approving — e.g. an email you propose to send. They can edit it (in the dashboard or by replying in the Slack thread); on approval the final text comes back in `output.content`. Use this when you want a human to review/refine content, not just say yes/no.'),
151
+ tool_name: z
152
+ .string()
153
+ .optional()
154
+ .describe('The tool/function this decision gates, e.g. "issue_refund" or "delete_database". Shown to the reviewer as a structured "Tool call" panel — the concrete action you would take if approved.'),
155
+ tool_args: z
156
+ .record(z.unknown())
157
+ .optional()
158
+ .describe('The arguments you would call `tool_name` with, as an object, e.g. { "amount_cents": 600000, "currency": "USD" }. Displayed next to the tool name. Ignored unless `tool_name` is set.'),
159
+ }, async ({ title, question, context, timeout_minutes, response_type, options, allow_multiple, proposed_output, tool_name, tool_args }) => {
160
+ if (response_type === 'select' && (!options || options.length < 2)) {
161
+ return asToolResult({
162
+ error: 'invalid_request',
163
+ message: "response_type 'select' requires at least 2 options.",
164
+ });
165
+ }
166
+ const responseSpec = buildResponseSpec(response_type, options, allow_multiple);
167
+ const externalRunId = `mcp-${Date.now()}-${Math.round(Math.random() * 1e6)}`;
168
+ // Structured context blocks (FN-83): a tool_call panel when the agent names
169
+ // the tool it's gating, plus any free-text context. Order = display order.
170
+ const contextBlocks = [];
171
+ if (tool_name) {
172
+ contextBlocks.push({ kind: 'tool_call', title: 'Tool call', tool: tool_name, args: tool_args ?? {} });
173
+ }
174
+ if (context) {
175
+ contextBlocks.push({ kind: 'text', title, text: context });
176
+ }
177
+ const res = await api('/v1/worker/decisions', {
178
+ method: 'POST',
179
+ body: JSON.stringify({
180
+ // The agent is its own "engine" — it polls (check/await) for the
181
+ // verdict rather than parking on an engine resume event.
182
+ engine: 'mcp',
183
+ externalRunId,
184
+ stepRef: title.slice(0, 80),
185
+ workflowRef: 'ratifia-mcp',
186
+ ...(APPROVER_EMAIL ? { assigneeEmail: APPROVER_EMAIL } : {}),
187
+ ...(POLICY ? { policy: POLICY } : {}),
188
+ required: true,
189
+ ...(timeout_minutes ? { timeoutSec: timeout_minutes * 60 } : {}),
190
+ ...(responseSpec ? { responseSpec } : {}),
191
+ ...(proposed_output
192
+ ? { proposedOutput: { title: proposed_output.title, format: 'text', content: proposed_output.content } }
193
+ : {}),
194
+ context: {
195
+ prompt: question,
196
+ ...(contextBlocks.length ? { blocks: contextBlocks } : {}),
197
+ },
198
+ }),
199
+ });
200
+ // The policy gate can resolve "no approval needed" — no decision is created.
201
+ // Tell the agent it may just proceed, rather than returning a bogus id/url.
202
+ if (res.approvalNeeded === false || !res.decisionId) {
203
+ return asToolResult({
204
+ approval_required: false,
205
+ status: 'not_required',
206
+ reason: res.reason ?? 'policy_gate_not_required',
207
+ note: 'Ratifia policy did not require a human decision — you may proceed.',
208
+ });
209
+ }
210
+ return asToolResult({
211
+ approval_required: true,
212
+ decision_id: res.decisionId,
213
+ status: 'pending',
214
+ url: `${APP_URL}/decisions/${res.decisionId}`,
215
+ });
216
+ });
217
+ server.tool('check_decision', "Check a Ratifia decision's status ONCE (non-blocking). Returns 'pending' until the human acts, then 'approved'/'rejected' plus their note (their input/answer). Prefer await_decision if you just want to wait for the verdict.", {
218
+ decision_id: z.string().describe('The decision_id returned by request_decision.'),
219
+ }, async ({ decision_id }) => asToolResult(formatDecision(await getDecision(decision_id))));
220
+ server.tool('await_decision', "Block until a Ratifia decision resolves (or max_wait_seconds elapses), so you don't have to poll in a loop. Returns as soon as the human acts ('approved'/'rejected' + their note), or status 'pending' with timed_out=true if still unanswered after the wait — just call await_decision again to keep waiting. Use this after request_decision to wait for the verdict.", {
221
+ decision_id: z.string().describe('The decision_id returned by request_decision.'),
222
+ max_wait_seconds: z
223
+ .number()
224
+ .int()
225
+ .positive()
226
+ .max(240)
227
+ .optional()
228
+ .describe('How long to block before returning (default 60, max 240). If still pending, call again to keep waiting.'),
229
+ }, async ({ decision_id, max_wait_seconds }) => {
230
+ const budgetMs = (max_wait_seconds ?? 60) * 1000;
231
+ const pollMs = 4000;
232
+ const start = Date.now();
233
+ let d = await getDecision(decision_id);
234
+ while (d.status === 'PENDING' && Date.now() - start + pollMs <= budgetMs) {
235
+ await new Promise((r) => setTimeout(r, pollMs));
236
+ d = await getDecision(decision_id);
237
+ }
238
+ return asToolResult({
239
+ ...formatDecision(d),
240
+ ...(d.status === 'PENDING' ? { timed_out: true } : {}),
241
+ });
242
+ });
243
+ server.tool('list_pending_decisions', 'List the decisions you have outstanding with Ratifia (default: still-pending), newest first. Use to reconcile after a restart, or to see what you are waiting on before deciding whether to keep waiting or cancel.', {
244
+ status: z
245
+ .enum(['PENDING', 'APPROVED', 'REJECTED', 'EXPIRED', 'CANCELLED'])
246
+ .optional()
247
+ .describe('Filter by status. Omit for PENDING (your outstanding asks).'),
248
+ limit: z.number().int().positive().max(200).optional().describe('Max to return (default 50).'),
249
+ }, async ({ status, limit }) => {
250
+ const qs = new URLSearchParams({ status: status ?? 'PENDING' });
251
+ if (limit)
252
+ qs.set('limit', String(limit));
253
+ const res = await api(`/v1/worker/decisions?${qs.toString()}`, {
254
+ method: 'GET',
255
+ });
256
+ return asToolResult({ count: res.decisions.length, decisions: res.decisions });
257
+ });
258
+ server.tool('cancel_decision', 'Withdraw a decision you requested but no longer need (the task was aborted or the question became moot). Resolves it and stops notifying the human. Only PENDING decisions cancel; an already-answered one is returned unchanged (cancelled=false).', {
259
+ decision_id: z.string().describe('The decision_id returned by request_decision.'),
260
+ note: z.string().optional().describe('Optional reason, recorded on the decision.'),
261
+ }, async ({ decision_id, note }) => asToolResult(await api(`/v1/worker/decisions/${encodeURIComponent(decision_id)}/cancel`, {
262
+ method: 'POST',
263
+ body: JSON.stringify(note ? { note } : {}),
264
+ })));
265
+ const transport = new StdioServerTransport();
266
+ await server.connect(transport);
267
+ console.error(`ratifia-mcp connected → ${API_URL}`);
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@ratifia/mcp",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "description": "Ratifia MCP server — let an AI agent request a human decision/approval (HITL) mid-task and resume once it's answered. The agent becomes a Ratifia 'engine'.",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "hitl",
10
+ "human-in-the-loop",
11
+ "approval",
12
+ "ai-agent",
13
+ "ratifia"
14
+ ],
15
+ "homepage": "https://ratifia.com",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/themillhouse/ratifia-mcp.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/themillhouse/ratifia-mcp/issues"
22
+ },
23
+ "license": "MIT",
24
+ "bin": {
25
+ "ratifia-mcp": "./dist/index.js"
26
+ },
27
+ "main": "./dist/index.js",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "dev": "tsx src/index.ts",
40
+ "start": "node dist/index.js",
41
+ "prepare": "tsc"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.0",
45
+ "zod": "^3.23.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.0.0",
49
+ "tsx": "^4.19.0",
50
+ "typescript": "^5.6.0"
51
+ }
52
+ }