@clearedby/mcp 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/tools.d.ts +23 -0
- package/dist/tools.js +73 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Morecheckouts Limited
|
|
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,43 @@
|
|
|
1
|
+
# @clearedby/mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an AI agent (Claude Code/Desktop, or any MCP client) gate its own consequential actions through [ClearedBy](https://clearedby.com).
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | What it does |
|
|
8
|
+
|------|--------------|
|
|
9
|
+
| `request_clearance` | Gate an action. Returns **cleared** (safe to proceed), **rejected** (do NOT proceed, with the reason), or — if held for a human — waits up to 10 minutes for the decision. |
|
|
10
|
+
| `check_policy` | Dry-run: preview what the policy *would* decide, recording nothing. |
|
|
11
|
+
| `get_ledger` | Recent entries from the tamper-evident attestation ledger. |
|
|
12
|
+
|
|
13
|
+
## Run it
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
CLEAREDBY_API_KEY=cb_live_… npx @clearedby/mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional: `CLEAREDBY_BASE_URL` (defaults to `https://app.clearedby.com`).
|
|
20
|
+
|
|
21
|
+
### Claude Desktop / Claude Code config
|
|
22
|
+
|
|
23
|
+
```jsonc
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"clearedby": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["@clearedby/mcp"],
|
|
29
|
+
"env": { "CLEAREDBY_API_KEY": "cb_live_…" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Example
|
|
36
|
+
|
|
37
|
+
> **Agent:** I'm about to issue a £400 refund on order SO-118.
|
|
38
|
+
> *calls `request_clearance { action: "refund.create", params: { amount: 400, order: "SO-118" } }`*
|
|
39
|
+
> **ClearedBy:** ⛔ Rejected by a reviewer: "Refund exceeds the 30-day window." Do NOT proceed.
|
|
40
|
+
|
|
41
|
+
The agent never decides for itself — policy clears the safe ones and a human handles the rest, and every decision is signed into your org's audit chain.
|
|
42
|
+
|
|
43
|
+
Built on [`@clearedby/sdk`](../sdk).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ClearedBy MCP server (CLE-20). Exposes three tools to any MCP client
|
|
3
|
+
// (Claude Code/Desktop, etc.): request_clearance, check_policy, get_ledger.
|
|
4
|
+
// Reads CLEAREDBY_API_KEY; talks to the gate via @clearedby/sdk over stdio.
|
|
5
|
+
//
|
|
6
|
+
// CLEAREDBY_API_KEY=cb_live_… npx @clearedby/mcp
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ClearedBy } from '@clearedby/sdk';
|
|
11
|
+
import { checkPolicy, getLedger, requestClearance } from './tools.js';
|
|
12
|
+
async function main() {
|
|
13
|
+
const apiKey = process.env.CLEAREDBY_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
console.error('CLEAREDBY_API_KEY is required');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const cb = new ClearedBy({ apiKey, baseUrl: process.env.CLEAREDBY_BASE_URL });
|
|
19
|
+
const server = new McpServer({ name: 'clearedby', version: '0.0.1' });
|
|
20
|
+
server.tool('request_clearance', 'Gate a consequential action before doing it. Returns whether it is cleared (safe to proceed), rejected (do NOT proceed), or — if held for a human — waits up to 10 minutes for a decision.', {
|
|
21
|
+
action: z.string().describe('e.g. refund.create, ad.launch'),
|
|
22
|
+
params: z.record(z.unknown()).optional().describe('the action parameters, e.g. { amount: 250 }'),
|
|
23
|
+
policy: z.string().optional(),
|
|
24
|
+
summary: z.string().optional().describe('one-line human summary for the reviewer'),
|
|
25
|
+
}, async (args) => {
|
|
26
|
+
const r = await requestClearance(cb, args);
|
|
27
|
+
return { content: [{ type: 'text', text: r.text }], isError: r.isError, structuredContent: r.structured };
|
|
28
|
+
});
|
|
29
|
+
server.tool('check_policy', 'Dry-run: preview what the policy WOULD decide for an action, without recording anything.', {
|
|
30
|
+
action: z.string(),
|
|
31
|
+
params: z.record(z.unknown()).optional(),
|
|
32
|
+
policy: z.string().optional(),
|
|
33
|
+
}, async (args) => {
|
|
34
|
+
const r = await checkPolicy(cb, args);
|
|
35
|
+
return { content: [{ type: 'text', text: r.text }], isError: r.isError, structuredContent: r.structured };
|
|
36
|
+
});
|
|
37
|
+
server.tool('get_ledger', 'Recent entries from the tamper-evident attestation ledger (signed decisions), newest first.', { limit: z.number().int().positive().max(200).optional() }, async (args) => {
|
|
38
|
+
const r = await getLedger(cb, args);
|
|
39
|
+
return { content: [{ type: 'text', text: r.text }], isError: r.isError, structuredContent: r.structured };
|
|
40
|
+
});
|
|
41
|
+
await server.connect(new StdioServerTransport());
|
|
42
|
+
}
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error('clearedby-mcp fatal:', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClearedBy } from '@clearedby/sdk';
|
|
2
|
+
export interface ToolResult {
|
|
3
|
+
text: string;
|
|
4
|
+
isError?: boolean;
|
|
5
|
+
structured: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** request_clearance: gate the action; if held, wait for a human (≤10 min). */
|
|
8
|
+
export declare function requestClearance(cb: ClearedBy, args: {
|
|
9
|
+
action: string;
|
|
10
|
+
params?: Record<string, unknown>;
|
|
11
|
+
policy?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
}): Promise<ToolResult>;
|
|
14
|
+
/** check_policy: dry-run — what would the policy do, with nothing persisted. */
|
|
15
|
+
export declare function checkPolicy(cb: ClearedBy, args: {
|
|
16
|
+
action: string;
|
|
17
|
+
params?: Record<string, unknown>;
|
|
18
|
+
policy?: string;
|
|
19
|
+
}): Promise<ToolResult>;
|
|
20
|
+
/** get_ledger: recent signed attestations. */
|
|
21
|
+
export declare function getLedger(cb: ClearedBy, args: {
|
|
22
|
+
limit?: number;
|
|
23
|
+
}): Promise<ToolResult>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Tool logic for the ClearedBy MCP server (CLE-20), kept pure + framework-free
|
|
2
|
+
// so it's unit-testable. Each returns { text, cleared?, result } which index.ts
|
|
3
|
+
// maps to MCP content. Built on @clearedby/sdk.
|
|
4
|
+
import { RejectedError } from '@clearedby/sdk';
|
|
5
|
+
const hash8 = (h) => (h ? ` · attestation ${h.slice(0, 8)}` : '');
|
|
6
|
+
/** request_clearance: gate the action; if held, wait for a human (≤10 min). */
|
|
7
|
+
export async function requestClearance(cb, args) {
|
|
8
|
+
try {
|
|
9
|
+
const r = await cb.gate({
|
|
10
|
+
action: args.action,
|
|
11
|
+
params: args.params ?? {},
|
|
12
|
+
...(args.policy ? { policy: args.policy } : {}),
|
|
13
|
+
...(args.summary ? { context: { summary: args.summary } } : {}),
|
|
14
|
+
});
|
|
15
|
+
if (r.shadow) {
|
|
16
|
+
return { text: `Shadow mode — not blocking. Would have been: ${r.would?.verdict ?? 'n/a'}.`, structured: { cleared: true, shadow: true, result: r } };
|
|
17
|
+
}
|
|
18
|
+
if (r.status === 'cleared') {
|
|
19
|
+
return { text: `✅ Cleared${hash8(r.attestation?.hash)}. Safe to proceed.`, structured: { cleared: true, result: r } };
|
|
20
|
+
}
|
|
21
|
+
if (r.status === 'rejected') {
|
|
22
|
+
return { text: `⛔ Rejected${r.reason ? `: ${r.reason}` : ''}. Do NOT proceed.`, isError: false, structured: { cleared: false, reason: r.reason ?? null, result: r } };
|
|
23
|
+
}
|
|
24
|
+
// pending → a human must decide. Sync-wait up to a configurable window
|
|
25
|
+
// (CLEAREDBY_MCP_WAIT_MS, default 10 min). For long / out-of-hours waits
|
|
26
|
+
// prefer a callback_url (park-and-resume) over holding this call open
|
|
27
|
+
// (CLE-99) — the item stays open either way; only the wait differs.
|
|
28
|
+
const waitMs = Number(process.env.CLEAREDBY_MCP_WAIT_MS) || 10 * 60_000;
|
|
29
|
+
let settled;
|
|
30
|
+
try {
|
|
31
|
+
settled = await cb.wait(r.id, { timeoutMs: waitMs });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Timed out while still pending — NOT a rejection. The item stays open
|
|
35
|
+
// for a human; surface that honestly rather than as a failure.
|
|
36
|
+
return {
|
|
37
|
+
text: `⏳ Still awaiting a human after ${Math.round(waitMs / 60000)} min — request ${r.id} stays open (not rejected). Re-check later, or pass a callback_url to be notified when it's decided.`,
|
|
38
|
+
structured: { cleared: false, pending: true, id: r.id, result: r },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (settled.status === 'cleared') {
|
|
42
|
+
return { text: `✅ Approved by a reviewer${hash8(settled.attestation?.hash)}. Safe to proceed.`, structured: { cleared: true, result: settled } };
|
|
43
|
+
}
|
|
44
|
+
return { text: `⛔ Rejected by a reviewer${settled.reason ? `: ${settled.reason}` : ''}. Do NOT proceed.`, structured: { cleared: false, reason: settled.reason ?? null, result: settled } };
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof RejectedError) {
|
|
48
|
+
return { text: `⛔ Rejected: ${err.result.reason ?? 'no reason given'}. Do NOT proceed.`, structured: { cleared: false, result: err.result } };
|
|
49
|
+
}
|
|
50
|
+
return { text: `Clearance check failed: ${err instanceof Error ? err.message : 'unknown error'}`, isError: true, structured: { cleared: false } };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** check_policy: dry-run — what would the policy do, with nothing persisted. */
|
|
54
|
+
export async function checkPolicy(cb, args) {
|
|
55
|
+
try {
|
|
56
|
+
const r = await cb.check({ action: args.action, params: args.params ?? {}, ...(args.policy ? { policy: args.policy } : {}) });
|
|
57
|
+
return { text: `Dry run: would be **${r.verdict}** (rule: ${r.rule}). Nothing was recorded.`, structured: { ...r } };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { text: `Policy check failed: ${err instanceof Error ? err.message : 'unknown error'}`, isError: true, structured: {} };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** get_ledger: recent signed attestations. */
|
|
64
|
+
export async function getLedger(cb, args) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await cb.ledger({ limit: args.limit ?? 20 });
|
|
67
|
+
const n = Array.isArray(r.entries) ? r.entries.length : 0;
|
|
68
|
+
return { text: `${n} recent attestation${n === 1 ? '' : 's'} (newest first).`, structured: { ...r } };
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return { text: `Ledger fetch failed: ${err instanceof Error ? err.message : 'unknown error'}`, isError: true, structured: {} };
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clearedby/mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ClearedBy MCP server — request_clearance, check_policy, get_ledger from any MCP client.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://clearedby.com",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"clearedby-mcp": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"clearedby",
|
|
22
|
+
"approval",
|
|
23
|
+
"human-in-the-loop",
|
|
24
|
+
"ai-agents"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/nl80/clearedby.git",
|
|
29
|
+
"directory": "packages/mcp"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
33
|
+
"zod": "^3.24.1",
|
|
34
|
+
"@clearedby/sdk": "0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.19.43",
|
|
38
|
+
"typescript": "^5.7.2",
|
|
39
|
+
"vitest": "^2.1.8"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
},
|
|
49
|
+
"types": "./dist/index.d.ts"
|
|
50
|
+
}
|