@taplid/mcp 0.4.6
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/README.md +165 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +66 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +31 -0
- package/dist/input-schema.d.ts +42 -0
- package/dist/input-schema.js +33 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +62 -0
- package/dist/mcp-output.d.ts +4 -0
- package/dist/mcp-output.js +13 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +35 -0
- package/dist/taplid-client.d.ts +18 -0
- package/dist/taplid-client.js +116 -0
- package/dist/tool.d.ts +7 -0
- package/dist/tool.js +64 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @taplid/mcp
|
|
2
|
+
|
|
3
|
+
Local MCP (Model Context Protocol) server exposing Taplid's artifact audit as a single tool for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Use Taplid from Claude Desktop, Cursor, or any MCP-aware client to verify AI-generated artifacts (implementation plans, code reviews, PR reviews, generated technical reports, proposed code changes) against supplied context before trusting, applying, merging, sending, or acting on them.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Registers one MCP tool, `taplid_audit`, that accepts three text fields and returns ALLOW / REVIEW / BLOCK with a 0-100 trust score and a structured summary.
|
|
10
|
+
|
|
11
|
+
- One tool only. No file reading. No command execution. No repo scanning.
|
|
12
|
+
- Stdio transport. Local process launched by the MCP client.
|
|
13
|
+
- HTTP caller boundary to the existing Taplid API. No engine code is imported.
|
|
14
|
+
|
|
15
|
+
## Install and configure
|
|
16
|
+
|
|
17
|
+
The package is invoked as a local binary by an MCP-aware client (Claude Desktop, Cursor, others). It does not need to be globally installed; `npx` resolves it on first invocation.
|
|
18
|
+
|
|
19
|
+
### Claude Desktop / Cursor (cross-platform)
|
|
20
|
+
|
|
21
|
+
Add this block to your MCP client's `mcpServers` config and `npx` will resolve the package on first invocation:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"taplid": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@taplid/mcp"],
|
|
29
|
+
"env": {
|
|
30
|
+
"TAPLID_API_KEY": "tap_live_...",
|
|
31
|
+
"TAPLID_API_URL": "https://api.taplid.com"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Local development (Windows path)
|
|
39
|
+
|
|
40
|
+
For local development against a Taplid API running on your machine:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"taplid-local": {
|
|
46
|
+
"command": "node",
|
|
47
|
+
"args": ["C:\\code\\taplid\\packages\\taplid-mcp\\dist\\server.js"],
|
|
48
|
+
"env": {
|
|
49
|
+
"TAPLID_API_KEY": "tap_live_...",
|
|
50
|
+
"TAPLID_API_URL": "http://127.0.0.1:7000"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Compatible with MCP-aware clients such as Claude Desktop and Cursor where stdio MCP servers are supported. Specific clients may use their own config formats.
|
|
58
|
+
|
|
59
|
+
## Environment variables
|
|
60
|
+
|
|
61
|
+
| Variable | Default | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `TAPLID_API_KEY` | required | Bearer token for the Taplid API. Set this in your MCP client config block. Never passed as a tool input. |
|
|
64
|
+
| `TAPLID_API_URL` | falls back to `TAPLID_PUBLIC_API_URL`, then `https://api.taplid.com` | Base URL for the Taplid API. |
|
|
65
|
+
| `TAPLID_MCP_TIMEOUT_MS` | `60000` | AbortController deadline for each request. Range 1000-600000ms; out-of-range values revert to default. |
|
|
66
|
+
| `TAPLID_MCP_DEBUG` | `false` | When `"true"`, successful responses include a `_debug` object with `{latencyMs, bytesIn, httpStatus}`. Bodies and secrets are never included. |
|
|
67
|
+
| `TAPLID_MCP_LOG_LEVEL` | `info` | Pino log level: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. |
|
|
68
|
+
|
|
69
|
+
Logs go to stderr only. Stdout is reserved for the MCP protocol.
|
|
70
|
+
|
|
71
|
+
## Tool: `taplid_audit`
|
|
72
|
+
|
|
73
|
+
`MAX_REVIEW_FIELD_CHARS` characters (120,000). Oversize input is rejected before the network call.
|
|
74
|
+
|
|
75
|
+
`TAPLID_API_KEY` is read from the MCP client env block.
|
|
76
|
+
|
|
77
|
+
### Worked example
|
|
78
|
+
|
|
79
|
+
Input:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"context": "The number is 1.",
|
|
84
|
+
"prompt": "What is the number?",
|
|
85
|
+
"response": "The number is 2."
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Expected envelope:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"auditId": "AUD-XXX...",
|
|
94
|
+
"decision": "BLOCK",
|
|
95
|
+
"trustScore": 0,
|
|
96
|
+
"summary": "This answer conflicts with the provided context.",
|
|
97
|
+
"issues": [
|
|
98
|
+
{
|
|
99
|
+
"message": "Contradicts the provided context.",
|
|
100
|
+
"snippet": "The number is 2.",
|
|
101
|
+
"reason": "The answer says the opposite of what the context says."
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"nextStep": "Do not use this yet. Adjust the answer to match the provided context, then re-run the check.",
|
|
105
|
+
"repairActions": [
|
|
106
|
+
{
|
|
107
|
+
"action": "Rewrite the answer so it aligns with the provided context.",
|
|
108
|
+
"priority": "high",
|
|
109
|
+
"target": "response"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"claims": [
|
|
113
|
+
{
|
|
114
|
+
"status": "contradicted",
|
|
115
|
+
"text": "The number is 2."
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"details": {
|
|
119
|
+
"passThreshold": 80,
|
|
120
|
+
"reviewThreshold": 60
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Response fields
|
|
126
|
+
|
|
127
|
+
- **auditId** — unique identifier for this audit run
|
|
128
|
+
- **decision** — `ALLOW`, `REVIEW`, or `BLOCK`
|
|
129
|
+
- **trustScore** — 0 to 100 public trust signal
|
|
130
|
+
- **summary** — short explanation for the verdict
|
|
131
|
+
- **issues** — concrete problems found in the response
|
|
132
|
+
- **nextStep** — practical guidance for what to do next
|
|
133
|
+
- **repairActions** — prioritized steps to fix the response
|
|
134
|
+
- **claims** — individual claims extracted and verified against the context
|
|
135
|
+
- **details.passThreshold / details.reviewThreshold** — decision thresholds set on your account
|
|
136
|
+
|
|
137
|
+
`repairActions` and `auditId` are present only when the API returns them.
|
|
138
|
+
|
|
139
|
+
## Error codes
|
|
140
|
+
|
|
141
|
+
The MCP tool returns structured errors with stable codes; no stack traces, no raw upstream bodies.
|
|
142
|
+
|
|
143
|
+
| Code | When |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `INVALID_INPUT` | A required field is missing or not a non-empty string. |
|
|
146
|
+
| `INPUT_TOO_LARGE` | One of `context`, `prompt`, `response` exceeds 120,000 characters. Detected pre-network. |
|
|
147
|
+
| `MISSING_API_KEY` | `TAPLID_API_KEY` is not set. Detected pre-network. |
|
|
148
|
+
| `UPSTREAM_AUTH_FAILED` | The Taplid API returned 401 or 403. |
|
|
149
|
+
| `UPSTREAM_RATE_LIMITED` | The Taplid API returned 429. |
|
|
150
|
+
| `UPSTREAM_TIMEOUT` | The request did not complete within `TAPLID_MCP_TIMEOUT_MS`. |
|
|
151
|
+
| `UPSTREAM_UNAVAILABLE` | The Taplid API returned 5xx or the host was unreachable. |
|
|
152
|
+
| `UPSTREAM_BAD_RESPONSE` | The Taplid API returned a body that was not valid JSON or not the expected shape. |
|
|
153
|
+
| `MCP_INTERNAL` | Unexpected internal error. |
|
|
154
|
+
|
|
155
|
+
## Security model
|
|
156
|
+
|
|
157
|
+
- One MCP tool. No file I/O, no shell, no process spawning, no inbound network listener.
|
|
158
|
+
- Stdio transport launched by the MCP client. The threat model includes hostile tool input and hostile generated artifacts.
|
|
159
|
+
- `TAPLID_API_KEY` is read from the environment only and is never accepted as a tool input.
|
|
160
|
+
- Logs redact the API key, the `Authorization` header, and the bodies of `context`, `prompt`, `response`. Stdout is reserved for the MCP protocol.
|
|
161
|
+
- The package never imports core Taplid engine code. The audit pipeline is reached via the public `/api/review` HTTP endpoint, which preserves all server-side guards.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
Same license as the parent Taplid workspace.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ResolvedConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
debug: boolean;
|
|
6
|
+
logLevel: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RawEnv {
|
|
9
|
+
TAPLID_API_URL?: string | undefined;
|
|
10
|
+
TAPLID_PUBLIC_API_URL?: string | undefined;
|
|
11
|
+
TAPLID_API_KEY?: string | undefined;
|
|
12
|
+
TAPLID_MCP_TIMEOUT_MS?: string | undefined;
|
|
13
|
+
TAPLID_MCP_DEBUG?: string | undefined;
|
|
14
|
+
TAPLID_MCP_LOG_LEVEL?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveConfigOrThrow(envOverride?: RawEnv): ResolvedConfig;
|
|
17
|
+
export declare function resolveConfigForLogger(envOverride?: RawEnv): {
|
|
18
|
+
debug: boolean;
|
|
19
|
+
logLevel: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { McpToolError } from './errors.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.taplid.com';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
4
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
5
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
6
|
+
function readEnv() {
|
|
7
|
+
return {
|
|
8
|
+
TAPLID_API_URL: process.env['TAPLID_API_URL'],
|
|
9
|
+
TAPLID_PUBLIC_API_URL: process.env['TAPLID_PUBLIC_API_URL'],
|
|
10
|
+
TAPLID_API_KEY: process.env['TAPLID_API_KEY'],
|
|
11
|
+
TAPLID_MCP_TIMEOUT_MS: process.env['TAPLID_MCP_TIMEOUT_MS'],
|
|
12
|
+
TAPLID_MCP_DEBUG: process.env['TAPLID_MCP_DEBUG'],
|
|
13
|
+
TAPLID_MCP_LOG_LEVEL: process.env['TAPLID_MCP_LOG_LEVEL'],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function pickBaseUrl(env) {
|
|
17
|
+
const candidates = [env.TAPLID_API_URL, env.TAPLID_PUBLIC_API_URL];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (typeof candidate === 'string') {
|
|
20
|
+
const trimmed = candidate.trim();
|
|
21
|
+
if (trimmed.length > 0)
|
|
22
|
+
return trimmed.replace(/\/+$/u, '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return DEFAULT_BASE_URL;
|
|
26
|
+
}
|
|
27
|
+
function pickTimeoutMs(env) {
|
|
28
|
+
const raw = env.TAPLID_MCP_TIMEOUT_MS;
|
|
29
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
30
|
+
return DEFAULT_TIMEOUT_MS;
|
|
31
|
+
const parsed = Number.parseInt(raw, 10);
|
|
32
|
+
if (!Number.isFinite(parsed) || parsed < MIN_TIMEOUT_MS || parsed > MAX_TIMEOUT_MS) {
|
|
33
|
+
return DEFAULT_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
function pickDebug(env) {
|
|
38
|
+
return env.TAPLID_MCP_DEBUG === 'true';
|
|
39
|
+
}
|
|
40
|
+
function pickLogLevel(env) {
|
|
41
|
+
const allowed = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent']);
|
|
42
|
+
const raw = env.TAPLID_MCP_LOG_LEVEL?.trim().toLowerCase();
|
|
43
|
+
if (raw !== undefined && allowed.has(raw))
|
|
44
|
+
return raw;
|
|
45
|
+
return 'info';
|
|
46
|
+
}
|
|
47
|
+
export function resolveConfigOrThrow(envOverride) {
|
|
48
|
+
const env = envOverride ?? readEnv();
|
|
49
|
+
const apiKeyRaw = env.TAPLID_API_KEY;
|
|
50
|
+
const apiKey = typeof apiKeyRaw === 'string' ? apiKeyRaw.trim() : '';
|
|
51
|
+
if (apiKey.length === 0) {
|
|
52
|
+
throw new McpToolError('MISSING_API_KEY', 'TAPLID_API_KEY environment variable is not set. Configure it in your MCP client config block.');
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
baseUrl: pickBaseUrl(env),
|
|
56
|
+
apiKey,
|
|
57
|
+
timeoutMs: pickTimeoutMs(env),
|
|
58
|
+
debug: pickDebug(env),
|
|
59
|
+
logLevel: pickLogLevel(env),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function resolveConfigForLogger(envOverride) {
|
|
63
|
+
const env = envOverride ?? readEnv();
|
|
64
|
+
return { debug: pickDebug(env), logLevel: pickLogLevel(env) };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=config.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const MCP_ERROR_CODES: readonly ["INVALID_INPUT", "INPUT_TOO_LARGE", "MISSING_API_KEY", "UPSTREAM_AUTH_FAILED", "UPSTREAM_RATE_LIMITED", "UPSTREAM_TIMEOUT", "UPSTREAM_UNAVAILABLE", "UPSTREAM_BAD_RESPONSE", "MCP_INTERNAL"];
|
|
2
|
+
export type McpErrorCode = (typeof MCP_ERROR_CODES)[number];
|
|
3
|
+
export declare class McpToolError extends Error {
|
|
4
|
+
readonly code: McpErrorCode;
|
|
5
|
+
readonly publicMessage: string;
|
|
6
|
+
constructor(code: McpErrorCode, publicMessage: string);
|
|
7
|
+
}
|
|
8
|
+
export interface McpErrorPayload {
|
|
9
|
+
code: McpErrorCode;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function mcpErrorPayload(err: McpToolError): McpErrorPayload;
|
|
13
|
+
export declare function errorName(err: unknown): string;
|
|
14
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const MCP_ERROR_CODES = [
|
|
2
|
+
'INVALID_INPUT',
|
|
3
|
+
'INPUT_TOO_LARGE',
|
|
4
|
+
'MISSING_API_KEY',
|
|
5
|
+
'UPSTREAM_AUTH_FAILED',
|
|
6
|
+
'UPSTREAM_RATE_LIMITED',
|
|
7
|
+
'UPSTREAM_TIMEOUT',
|
|
8
|
+
'UPSTREAM_UNAVAILABLE',
|
|
9
|
+
'UPSTREAM_BAD_RESPONSE',
|
|
10
|
+
'MCP_INTERNAL',
|
|
11
|
+
];
|
|
12
|
+
export class McpToolError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
publicMessage;
|
|
15
|
+
constructor(code, publicMessage) {
|
|
16
|
+
super(publicMessage);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.publicMessage = publicMessage;
|
|
19
|
+
this.name = 'McpToolError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function mcpErrorPayload(err) {
|
|
23
|
+
return { code: err.code, message: err.publicMessage };
|
|
24
|
+
}
|
|
25
|
+
export function errorName(err) {
|
|
26
|
+
if (err instanceof Error && typeof err.name === 'string' && err.name.length > 0) {
|
|
27
|
+
return err.name;
|
|
28
|
+
}
|
|
29
|
+
return 'UnknownError';
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const TAPLID_AUDIT_INPUT_SHAPE: {
|
|
3
|
+
readonly context: z.ZodString;
|
|
4
|
+
readonly prompt: z.ZodString;
|
|
5
|
+
readonly response: z.ZodString;
|
|
6
|
+
};
|
|
7
|
+
export declare const TAPLID_AUDIT_INPUT: z.ZodObject<{
|
|
8
|
+
readonly context: z.ZodString;
|
|
9
|
+
readonly prompt: z.ZodString;
|
|
10
|
+
readonly response: z.ZodString;
|
|
11
|
+
}, "strict", z.ZodTypeAny, {
|
|
12
|
+
context: string;
|
|
13
|
+
prompt: string;
|
|
14
|
+
response: string;
|
|
15
|
+
}, {
|
|
16
|
+
context: string;
|
|
17
|
+
prompt: string;
|
|
18
|
+
response: string;
|
|
19
|
+
}>;
|
|
20
|
+
export type TaplidAuditInput = z.infer<typeof TAPLID_AUDIT_INPUT>;
|
|
21
|
+
export declare const TOOL_INPUT_JSON_SCHEMA: {
|
|
22
|
+
type: "object";
|
|
23
|
+
properties: {
|
|
24
|
+
context: {
|
|
25
|
+
type: "string";
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
prompt: {
|
|
29
|
+
type: "string";
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
response: {
|
|
33
|
+
type: "string";
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
required: readonly ["context", "prompt", "response"];
|
|
38
|
+
additionalProperties: false;
|
|
39
|
+
};
|
|
40
|
+
export declare const TOOL_DESCRIPTION: string;
|
|
41
|
+
export declare const TOOL_NAME = "taplid_audit";
|
|
42
|
+
//# sourceMappingURL=input-schema.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MAX_REVIEW_FIELD_CHARS } from '@taplid/contract';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const TAPLID_AUDIT_INPUT_SHAPE = {
|
|
4
|
+
context: z.string().min(1).max(MAX_REVIEW_FIELD_CHARS),
|
|
5
|
+
prompt: z.string().min(1).max(MAX_REVIEW_FIELD_CHARS),
|
|
6
|
+
response: z.string().min(1).max(MAX_REVIEW_FIELD_CHARS),
|
|
7
|
+
};
|
|
8
|
+
export const TAPLID_AUDIT_INPUT = z.object(TAPLID_AUDIT_INPUT_SHAPE).strict();
|
|
9
|
+
export const TOOL_INPUT_JSON_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
context: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'The source material the artifact must be consistent with (diff, spec, code, docs).',
|
|
15
|
+
},
|
|
16
|
+
prompt: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The prompt or task that produced the artifact.',
|
|
19
|
+
},
|
|
20
|
+
response: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The AI-generated artifact to verify.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ['context', 'prompt', 'response'],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
};
|
|
28
|
+
export const TOOL_DESCRIPTION = 'Use this before trusting, applying, merging, sending, or acting on an AI-generated artifact. ' +
|
|
29
|
+
'Suitable for code reviews, implementation plans, PR reviews, generated technical reports, ' +
|
|
30
|
+
'proposed code changes, and other structured AI outputs. ' +
|
|
31
|
+
'Returns ALLOW / REVIEW / BLOCK with a 0-100 trust score and a structured summary.';
|
|
32
|
+
export const TOOL_NAME = 'taplid_audit';
|
|
33
|
+
//# sourceMappingURL=input-schema.js.map
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { resolveConfigForLogger } from './config.js';
|
|
3
|
+
const REDACT_PATHS = [
|
|
4
|
+
'authorization',
|
|
5
|
+
'Authorization',
|
|
6
|
+
'apiKey',
|
|
7
|
+
'api_key',
|
|
8
|
+
'TAPLID_API_KEY',
|
|
9
|
+
'context',
|
|
10
|
+
'prompt',
|
|
11
|
+
'response',
|
|
12
|
+
'*.authorization',
|
|
13
|
+
'*.Authorization',
|
|
14
|
+
'*.apiKey',
|
|
15
|
+
'*.api_key',
|
|
16
|
+
'*.TAPLID_API_KEY',
|
|
17
|
+
'*.context',
|
|
18
|
+
'*.prompt',
|
|
19
|
+
'*.response',
|
|
20
|
+
];
|
|
21
|
+
const stderrSink = {
|
|
22
|
+
write(chunk) {
|
|
23
|
+
process.stderr.write(chunk);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
function buildLogger() {
|
|
27
|
+
const cfg = resolveConfigForLogger();
|
|
28
|
+
return pino({
|
|
29
|
+
level: cfg.logLevel,
|
|
30
|
+
redact: { paths: REDACT_PATHS, censor: '[REDACTED]', remove: false },
|
|
31
|
+
base: { service: 'taplid-mcp' },
|
|
32
|
+
}, stderrSink);
|
|
33
|
+
}
|
|
34
|
+
export const logger = buildLogger();
|
|
35
|
+
export function runRedactSelfTest() {
|
|
36
|
+
const sentinel = `sk-self-test-secret-${Math.random().toString(36).slice(2)}`;
|
|
37
|
+
let captured = '';
|
|
38
|
+
const captureSink = {
|
|
39
|
+
write(chunk) {
|
|
40
|
+
captured += chunk;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const probe = pino({
|
|
44
|
+
// Force level=trace so the probe info() emits regardless of TAPLID_MCP_LOG_LEVEL.
|
|
45
|
+
level: 'trace',
|
|
46
|
+
redact: { paths: REDACT_PATHS, censor: '[REDACTED]', remove: false },
|
|
47
|
+
base: { service: 'taplid-mcp' },
|
|
48
|
+
}, captureSink);
|
|
49
|
+
probe.info({ event: 'redact.self-test', authorization: `Bearer ${sentinel}`, TAPLID_API_KEY: sentinel }, 'redact self-test');
|
|
50
|
+
if (captured.length === 0) {
|
|
51
|
+
throw new Error('[taplid-mcp] redact self-test FAILED: no probe record emitted. Refusing to start.');
|
|
52
|
+
}
|
|
53
|
+
if (captured.includes(sentinel)) {
|
|
54
|
+
throw new Error('[taplid-mcp] redact self-test FAILED: secret leaked to stderr. Refusing to start.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function truncateForLog(value, limit = 200) {
|
|
58
|
+
if (value.length <= limit)
|
|
59
|
+
return value;
|
|
60
|
+
return `${value.slice(0, limit)}[+${value.length - limit} chars]`;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function buildMcpOutput(result, cfg) {
|
|
2
|
+
const envelope = result.envelope;
|
|
3
|
+
if (!cfg.debug)
|
|
4
|
+
return envelope;
|
|
5
|
+
const debug = {
|
|
6
|
+
auditMode: 'artifact',
|
|
7
|
+
latencyMs: result.latencyMs,
|
|
8
|
+
bytesIn: result.bytesIn,
|
|
9
|
+
httpStatus: result.httpStatus,
|
|
10
|
+
};
|
|
11
|
+
return { ...envelope, _debug: debug };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=mcp-output.js.map
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { errorName } from './errors.js';
|
|
6
|
+
import { logger, runRedactSelfTest } from './logger.js';
|
|
7
|
+
import { TAPLID_AUDIT_INPUT_SHAPE, TOOL_DESCRIPTION, TOOL_NAME, } from './input-schema.js';
|
|
8
|
+
import { handleTaplidAudit } from './tool.js';
|
|
9
|
+
export function createServer() {
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'taplid-mcp',
|
|
12
|
+
version: '0.4.6',
|
|
13
|
+
});
|
|
14
|
+
server.registerTool(TOOL_NAME, {
|
|
15
|
+
description: TOOL_DESCRIPTION,
|
|
16
|
+
inputSchema: TAPLID_AUDIT_INPUT_SHAPE,
|
|
17
|
+
}, async (rawInput) => handleTaplidAudit(rawInput));
|
|
18
|
+
return server;
|
|
19
|
+
}
|
|
20
|
+
export async function main() {
|
|
21
|
+
runRedactSelfTest();
|
|
22
|
+
const server = createServer();
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
logger.info({ event: 'taplid_mcp.ready' });
|
|
26
|
+
}
|
|
27
|
+
const entry = process.argv[1];
|
|
28
|
+
const invokedDirectly = typeof entry === 'string' && entry.length > 0 && pathToFileURL(entry).href === import.meta.url;
|
|
29
|
+
if (invokedDirectly) {
|
|
30
|
+
main().catch((err) => {
|
|
31
|
+
logger.error({ event: 'taplid_mcp.fatal', errorName: errorName(err) });
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedConfig } from './config.js';
|
|
2
|
+
export interface ReviewRequest {
|
|
3
|
+
context: string;
|
|
4
|
+
prompt: string;
|
|
5
|
+
response: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ReviewClientDeps {
|
|
8
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export interface ReviewClientResult {
|
|
12
|
+
envelope: unknown;
|
|
13
|
+
latencyMs: number;
|
|
14
|
+
bytesIn: number;
|
|
15
|
+
httpStatus: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function postArtifactReview(req: ReviewRequest, cfg: ResolvedConfig, deps?: ReviewClientDeps): Promise<ReviewClientResult>;
|
|
18
|
+
//# sourceMappingURL=taplid-client.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { McpToolError } from './errors.js';
|
|
2
|
+
export async function postArtifactReview(req, cfg, deps = {}) {
|
|
3
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
4
|
+
const now = deps.now ?? (() => Date.now());
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const t0 = now();
|
|
7
|
+
let timer;
|
|
8
|
+
const timeoutSignal = new Promise((_resolve, reject) => {
|
|
9
|
+
timer = setTimeout(() => {
|
|
10
|
+
controller.abort();
|
|
11
|
+
reject(new McpToolError('UPSTREAM_TIMEOUT', `Taplid API did not respond within ${cfg.timeoutMs}ms. Check TAPLID_API_URL and network connectivity.`));
|
|
12
|
+
}, cfg.timeoutMs);
|
|
13
|
+
});
|
|
14
|
+
let res;
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
try {
|
|
18
|
+
res = await Promise.race([
|
|
19
|
+
fetchImpl(`${cfg.baseUrl}/api/review`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'content-type': 'application/json',
|
|
23
|
+
authorization: `Bearer ${cfg.apiKey}`,
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
auditMode: 'artifact',
|
|
27
|
+
context: req.context,
|
|
28
|
+
prompt: req.prompt,
|
|
29
|
+
response: req.response,
|
|
30
|
+
}),
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
}),
|
|
33
|
+
timeoutSignal,
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err instanceof McpToolError)
|
|
38
|
+
throw err;
|
|
39
|
+
const name = err?.name;
|
|
40
|
+
if (name === 'AbortError') {
|
|
41
|
+
throw new McpToolError('UPSTREAM_TIMEOUT', `Taplid API did not respond within ${cfg.timeoutMs}ms. Check TAPLID_API_URL and network connectivity.`);
|
|
42
|
+
}
|
|
43
|
+
throw new McpToolError('UPSTREAM_UNAVAILABLE', 'Taplid API is unreachable. Check TAPLID_API_URL and network connectivity.');
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
text = await Promise.race([res.text(), timeoutSignal]);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof McpToolError)
|
|
50
|
+
throw err;
|
|
51
|
+
const name = err?.name;
|
|
52
|
+
if (name === 'AbortError') {
|
|
53
|
+
throw new McpToolError('UPSTREAM_TIMEOUT', `Taplid API did not respond within ${cfg.timeoutMs}ms. Check TAPLID_API_URL and network connectivity.`);
|
|
54
|
+
}
|
|
55
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API returned an unreadable response body.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (timer !== undefined)
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
const latencyMs = now() - t0;
|
|
63
|
+
if (res.status === 401 || res.status === 403) {
|
|
64
|
+
throw new McpToolError('UPSTREAM_AUTH_FAILED', 'Taplid API rejected the API key.');
|
|
65
|
+
}
|
|
66
|
+
if (res.status === 429) {
|
|
67
|
+
throw new McpToolError('UPSTREAM_RATE_LIMITED', 'Taplid API rate limit exceeded. Retry later.');
|
|
68
|
+
}
|
|
69
|
+
if (res.status >= 500 && res.status < 600) {
|
|
70
|
+
throw new McpToolError('UPSTREAM_UNAVAILABLE', 'Taplid API returned a server error.');
|
|
71
|
+
}
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API returned an unexpected response.');
|
|
74
|
+
}
|
|
75
|
+
let envelope;
|
|
76
|
+
try {
|
|
77
|
+
envelope = JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API returned non-JSON.');
|
|
81
|
+
}
|
|
82
|
+
assertEnvelopeShape(envelope);
|
|
83
|
+
return {
|
|
84
|
+
envelope,
|
|
85
|
+
latencyMs,
|
|
86
|
+
bytesIn: Buffer.byteLength(text, 'utf8'),
|
|
87
|
+
httpStatus: res.status,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function assertEnvelopeShape(envelope) {
|
|
91
|
+
if (envelope === null || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
92
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response did not match the expected shape.');
|
|
93
|
+
}
|
|
94
|
+
const record = envelope;
|
|
95
|
+
const decision = record['decision'];
|
|
96
|
+
if (decision !== 'ALLOW' && decision !== 'REVIEW' && decision !== 'BLOCK') {
|
|
97
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response is missing a valid decision.');
|
|
98
|
+
}
|
|
99
|
+
const trustScore = record['trustScore'];
|
|
100
|
+
if (typeof trustScore !== 'number' || !Number.isFinite(trustScore)) {
|
|
101
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response is missing a valid trustScore.');
|
|
102
|
+
}
|
|
103
|
+
const summary = record['summary'];
|
|
104
|
+
if (typeof summary !== 'string') {
|
|
105
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response is missing a valid summary.');
|
|
106
|
+
}
|
|
107
|
+
const issues = record['issues'];
|
|
108
|
+
if (!Array.isArray(issues)) {
|
|
109
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response is missing a valid issues array.');
|
|
110
|
+
}
|
|
111
|
+
const nextStep = record['nextStep'];
|
|
112
|
+
if (typeof nextStep !== 'string') {
|
|
113
|
+
throw new McpToolError('UPSTREAM_BAD_RESPONSE', 'Taplid API response is missing a valid nextStep.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=taplid-client.js.map
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { type ReviewClientDeps } from './taplid-client.js';
|
|
3
|
+
export interface HandleAuditDeps extends ReviewClientDeps {
|
|
4
|
+
envOverride?: Record<string, string | undefined>;
|
|
5
|
+
}
|
|
6
|
+
export declare function handleTaplidAudit(rawInput: unknown, deps?: HandleAuditDeps): Promise<CallToolResult>;
|
|
7
|
+
//# sourceMappingURL=tool.d.ts.map
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveConfigOrThrow } from './config.js';
|
|
2
|
+
import { McpToolError, errorName, mcpErrorPayload } from './errors.js';
|
|
3
|
+
import { TAPLID_AUDIT_INPUT } from './input-schema.js';
|
|
4
|
+
import { logger, truncateForLog } from './logger.js';
|
|
5
|
+
import { buildMcpOutput } from './mcp-output.js';
|
|
6
|
+
import { postArtifactReview } from './taplid-client.js';
|
|
7
|
+
export async function handleTaplidAudit(rawInput, deps = {}) {
|
|
8
|
+
let cfg;
|
|
9
|
+
try {
|
|
10
|
+
cfg = resolveConfigOrThrow(deps.envOverride);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return toolErrorResult(err);
|
|
14
|
+
}
|
|
15
|
+
const parsed = TAPLID_AUDIT_INPUT.safeParse(rawInput);
|
|
16
|
+
if (!parsed.success) {
|
|
17
|
+
const tooLarge = parsed.error.issues.some((issue) => issue.code === 'too_big');
|
|
18
|
+
const code = tooLarge ? 'INPUT_TOO_LARGE' : 'INVALID_INPUT';
|
|
19
|
+
const message = tooLarge
|
|
20
|
+
? 'One of context/prompt/response exceeds the Taplid artifact-mode length limit.'
|
|
21
|
+
: 'taplid_audit requires non-empty string fields: context, prompt, response.';
|
|
22
|
+
return toolErrorResult(new McpToolError(code, message));
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await postArtifactReview(parsed.data, cfg, deps);
|
|
26
|
+
const envelope = buildMcpOutput(result, cfg);
|
|
27
|
+
const decision = typeof envelope.decision === 'string'
|
|
28
|
+
? envelope.decision
|
|
29
|
+
: 'unknown';
|
|
30
|
+
const trustScore = typeof envelope.trustScore === 'number'
|
|
31
|
+
? envelope.trustScore
|
|
32
|
+
: null;
|
|
33
|
+
logger.info({
|
|
34
|
+
event: 'taplid_audit.ok',
|
|
35
|
+
auditMode: 'artifact',
|
|
36
|
+
decision,
|
|
37
|
+
trustScore,
|
|
38
|
+
latencyMs: result.latencyMs,
|
|
39
|
+
bytesIn: result.bytesIn,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify(envelope) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return toolErrorResult(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function toolErrorResult(err) {
|
|
50
|
+
if (err instanceof McpToolError) {
|
|
51
|
+
logger.warn({ event: 'taplid_audit.err', errorCode: err.code });
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: JSON.stringify(mcpErrorPayload(err)) }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
logger.error({ event: 'taplid_audit.unhandled', errorName: truncateForLog(errorName(err), 80) });
|
|
58
|
+
const internal = new McpToolError('MCP_INTERNAL', 'Internal error inside the Taplid MCP server.');
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: JSON.stringify(mcpErrorPayload(internal)) }],
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=tool.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taplid/mcp",
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "Local MCP (Model Context Protocol) server exposing Taplid artifact audit as a single tool for AI coding agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"taplid",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"ai",
|
|
10
|
+
"llm",
|
|
11
|
+
"audit",
|
|
12
|
+
"trust-score",
|
|
13
|
+
"claude",
|
|
14
|
+
"cursor",
|
|
15
|
+
"ai-safety",
|
|
16
|
+
"guardrails",
|
|
17
|
+
"verification"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./dist/server.js",
|
|
22
|
+
"types": "./dist/server.d.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"taplid-mcp": "./dist/server.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/server.d.ts",
|
|
29
|
+
"import": "./dist/server.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/**/*.js",
|
|
34
|
+
"dist/**/*.d.ts",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
+
"@taplid/contract": "0.4.6",
|
|
40
|
+
"pino": "^9.0.0",
|
|
41
|
+
"zod": "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"prebuild": "npm --prefix ../.. --workspace @taplid/contract run build",
|
|
51
|
+
"build": "tsc -p tsconfig.json",
|
|
52
|
+
"pretest": "npm --prefix ../.. --workspace @taplid/contract run build",
|
|
53
|
+
"test": "vitest run -c vitest.config.ts",
|
|
54
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
55
|
+
"prepublishOnly": "npm --prefix ../.. run verify:pack:mcp",
|
|
56
|
+
"publish:public": "npm publish --access public"
|
|
57
|
+
}
|
|
58
|
+
}
|