@ductape/mcp 0.2.18 → 0.2.20
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/dist/action-contract.d.ts +2 -0
- package/dist/action-contract.d.ts.map +1 -0
- package/dist/action-contract.js +109 -0
- package/dist/index.js +24 -6
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-contract.d.ts","sourceRoot":"","sources":["../src/action-contract.ts"],"names":[],"mappings":"AAiFA,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAYnE"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function parseSample(value) {
|
|
2
|
+
if (typeof value !== 'string')
|
|
3
|
+
return value;
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function legacyPairs(value) {
|
|
12
|
+
const parsed = parseSample(value);
|
|
13
|
+
if (!Array.isArray(parsed))
|
|
14
|
+
return null;
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const item of parsed) {
|
|
17
|
+
if (!item || typeof item !== 'object')
|
|
18
|
+
continue;
|
|
19
|
+
const key = String(item.key ?? '').trim();
|
|
20
|
+
if (key)
|
|
21
|
+
result[key] = item.value;
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(result).length ? result : null;
|
|
24
|
+
}
|
|
25
|
+
function schemaType(value) {
|
|
26
|
+
const type = String(value ?? '').toLowerCase();
|
|
27
|
+
if (type.includes('bool'))
|
|
28
|
+
return 'boolean';
|
|
29
|
+
if (type.includes('int') || type.includes('number') || type.includes('float') || type.includes('double'))
|
|
30
|
+
return 'number';
|
|
31
|
+
if (type.includes('array'))
|
|
32
|
+
return 'array';
|
|
33
|
+
if (type.includes('object') || type === 'json')
|
|
34
|
+
return 'object';
|
|
35
|
+
if (type.includes('string') || type.includes('email') || type.includes('date') || type.includes('uuid') || type.includes('space'))
|
|
36
|
+
return 'string';
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function schemaFromLocation(value) {
|
|
40
|
+
const location = value && typeof value === 'object' ? value : {};
|
|
41
|
+
const parsedSample = parseSample(location.sample);
|
|
42
|
+
const legacyRows = Array.isArray(parsedSample) ? parsedSample : [];
|
|
43
|
+
const sample = legacyPairs(location.sample) ?? parsedSample;
|
|
44
|
+
const rows = Array.isArray(location.data) ? location.data : [];
|
|
45
|
+
const properties = {};
|
|
46
|
+
const required = [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (!row || typeof row !== 'object')
|
|
49
|
+
continue;
|
|
50
|
+
const item = row;
|
|
51
|
+
if (Number(item.level ?? 0) > 0 || String(item.parent_key ?? '').trim())
|
|
52
|
+
continue;
|
|
53
|
+
const key = String(item.key ?? item.name ?? '').trim();
|
|
54
|
+
if (!key)
|
|
55
|
+
continue;
|
|
56
|
+
const property = {};
|
|
57
|
+
const type = schemaType(item.type);
|
|
58
|
+
if (type)
|
|
59
|
+
property.type = type;
|
|
60
|
+
if (item.description)
|
|
61
|
+
property.description = String(item.description);
|
|
62
|
+
if (item.sampleValue !== undefined && item.sampleValue !== '')
|
|
63
|
+
property.example = item.sampleValue;
|
|
64
|
+
properties[key] = property;
|
|
65
|
+
if (item.required === true)
|
|
66
|
+
required.push(key);
|
|
67
|
+
}
|
|
68
|
+
for (const row of legacyRows) {
|
|
69
|
+
if (!row || typeof row !== 'object')
|
|
70
|
+
continue;
|
|
71
|
+
const item = row;
|
|
72
|
+
const key = String(item.key ?? '').trim();
|
|
73
|
+
if (!key || properties[key])
|
|
74
|
+
continue;
|
|
75
|
+
const description = String(item.description ?? '').trim();
|
|
76
|
+
properties[key] = {
|
|
77
|
+
type: 'string',
|
|
78
|
+
...(description ? { description } : {}),
|
|
79
|
+
...(item.value !== undefined ? { example: item.value } : {}),
|
|
80
|
+
};
|
|
81
|
+
if (/\(required\)|^required\b/i.test(description))
|
|
82
|
+
required.push(key);
|
|
83
|
+
}
|
|
84
|
+
if (!Object.keys(properties).length && sample && typeof sample === 'object' && !Array.isArray(sample)) {
|
|
85
|
+
for (const [key, example] of Object.entries(sample)) {
|
|
86
|
+
properties[key] = { type: Array.isArray(example) ? 'array' : example === null ? undefined : typeof example, example };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties,
|
|
92
|
+
...(required.length ? { required } : {}),
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function normalizeLiveActionContract(value) {
|
|
97
|
+
if (!value || typeof value !== 'object')
|
|
98
|
+
return value;
|
|
99
|
+
const action = value;
|
|
100
|
+
return {
|
|
101
|
+
...action,
|
|
102
|
+
input_schema: {
|
|
103
|
+
body: schemaFromLocation(action.body),
|
|
104
|
+
query: schemaFromLocation(action.query),
|
|
105
|
+
params: schemaFromLocation(action.params),
|
|
106
|
+
headers: schemaFromLocation(action.headers),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { parseCliCommand } from './cli-command.js';
|
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import { delimiter, join } from 'path';
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
+
import { normalizeLiveActionContract } from './action-contract.js';
|
|
18
19
|
import { executeViaProxy, generateExecutablePayload, getAssetSchemas, } from './proxy-client.js';
|
|
19
20
|
import { EVENTS_DELIVERY_SEMANTICS, EVENTS_IMPLEMENTATION_WARNING, eventsCapabilityOverview, inspectEventsComponent, searchEventsCapabilities, } from './events-capabilities.js';
|
|
20
21
|
const MODULES = [
|
|
@@ -596,7 +597,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
596
597
|
fallback.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }] ← CALL ductape_generate_payload FIRST (operation_family="fallback", method="dispatch") — requires redisUrl in ductape initialization
|
|
597
598
|
|
|
598
599
|
━━━ MODULE: health ━━━
|
|
599
|
-
health.create [product_tag, data: { tag: string, name: string, description?: string,
|
|
600
|
+
health.create [product_tag, data: { tag: string, name: string, description?: string, probe: { type: "app"|"database"|"feature"|"graph"|"events"|"storage"|"cache"|"vector"|"notification", app?: string, database?: string, feature?: string, graph?: string, events?: string, storage?: string, cache?: string, vector?: string, notification?: string, channels?: ("sms"|"email"|"push")[], event?: string, input?: object }, interval: number, retries: number, envs: [{ slug: string, input?: object }], onFailure?: object }]
|
|
600
601
|
health.update [product_tag, health_tag, data: { name?: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"feature", app?: string, event?: string, input?: object }, interval?: number, retries?: number, envs?: [{ slug: string, input?: object }], onFailure?: object }]
|
|
601
602
|
health.fetch [product_tag, health_tag]
|
|
602
603
|
health.list [product_tag]
|
|
@@ -3134,7 +3135,7 @@ CONFIGURATION BOUNDARY
|
|
|
3134
3135
|
Workbench is also supported. Never route their administrative create/update methods through
|
|
3135
3136
|
ductape_execute: its publishable-key runtime proxy will fail.
|
|
3136
3137
|
|
|
3137
|
-
QUOTAS —
|
|
3138
|
+
QUOTAS — weighted/provider-capacity routing pools (NOT request rate limiting):
|
|
3138
3139
|
Workbench definition shape:
|
|
3139
3140
|
{
|
|
3140
3141
|
tag: "sms-quota",
|
|
@@ -3152,6 +3153,8 @@ QUOTAS — rate-limited multi-provider pools:
|
|
|
3152
3153
|
],
|
|
3153
3154
|
}
|
|
3154
3155
|
Providers are tried in order until quota is not exhausted.
|
|
3156
|
+
Never use quota to protect OTP, login, or public endpoints. Use sdk.rateLimit.define/run and the
|
|
3157
|
+
Nest @RateLimit + DuctapeRateLimitGuard for atomic keyed request enforcement.
|
|
3155
3158
|
quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
3156
3159
|
quotas.dispatch [{ product, env, tag, input, schedule? }]
|
|
3157
3160
|
|
|
@@ -3188,7 +3191,13 @@ HEALTHCHECKS — continuous probe with failure notifications:
|
|
|
3188
3191
|
health.check [{ product, env, tag }] → same as run
|
|
3189
3192
|
health.status [{ product, env, tag }] → current health status
|
|
3190
3193
|
|
|
3191
|
-
Probe types: app | database | feature | graph | events | storage
|
|
3194
|
+
Probe types: app | database | feature | graph | events | storage | cache | vector | notification
|
|
3195
|
+
Code-first connectivity probes:
|
|
3196
|
+
ctx.probe().cache("cache-tag").action("test_connection")
|
|
3197
|
+
ctx.probe().vector("vector-tag").action("test_connection")
|
|
3198
|
+
ctx.probe().notification("notification-tag").channels(["sms","email","push"]).action("test_connection")
|
|
3199
|
+
Notification test_connection validates configuration and uses non-delivering credential/account
|
|
3200
|
+
checks for SendGrid, Vonage/Nexmo, and Firebase. It never sends a message.
|
|
3192
3201
|
Canonical Events probe:
|
|
3193
3202
|
{ type: "events", events: "statecraft-events", event: "health" }
|
|
3194
3203
|
Do not create new probes with type/field message_broker or messageBroker; those names are deprecated
|
|
@@ -3225,6 +3234,7 @@ Provider status: available | unavailable
|
|
|
3225
3234
|
|
|
3226
3235
|
DECISION MATRIX
|
|
3227
3236
|
Rate/capacity allocation across providers → quota
|
|
3237
|
+
Request frequency keyed by actor/IP/device → rateLimit
|
|
3228
3238
|
Equivalent provider after operational failure → fallback
|
|
3229
3239
|
Detect failure before routing provider traffic → health check
|
|
3230
3240
|
Transient failure of one operation → bounded retry + idempotency policy
|
|
@@ -5125,7 +5135,8 @@ function handleCliFlags() {
|
|
|
5125
5135
|
}
|
|
5126
5136
|
async function main() {
|
|
5127
5137
|
const { McpServer, StdioServerTransport } = await loadMcpSdk();
|
|
5128
|
-
const
|
|
5138
|
+
const { version: mcpVersion } = createRequire(import.meta.url)('../package.json');
|
|
5139
|
+
const server = new McpServer({ name: 'ductape-mcp', version: mcpVersion });
|
|
5129
5140
|
const transport = new StdioServerTransport();
|
|
5130
5141
|
const cliHandler = async (args) => {
|
|
5131
5142
|
const cli = checkCli();
|
|
@@ -5152,7 +5163,8 @@ async function main() {
|
|
|
5152
5163
|
const isAuthCommand = firstWord === 'login' || firstWord === 'logout';
|
|
5153
5164
|
const isLocalMigrationGuidance = (firstWord === 'migrate-codebase' && !args.command.includes('--ensure-product')) ||
|
|
5154
5165
|
firstWord.startsWith('migration-');
|
|
5155
|
-
|
|
5166
|
+
const isDiagnostic = firstWord === 'doctor';
|
|
5167
|
+
if (!isAuthCommand && !isLocalMigrationGuidance && !isDiagnostic) {
|
|
5156
5168
|
// Cache successful authentication, but re-check a missing/expired session on every call.
|
|
5157
5169
|
// The user may complete `ductape login` in another terminal while this MCP process remains
|
|
5158
5170
|
// alive; caching "none" would otherwise make the MCP blind to the newly written session.
|
|
@@ -5439,7 +5451,7 @@ async function main() {
|
|
|
5439
5451
|
const raw = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
5440
5452
|
let action = raw;
|
|
5441
5453
|
try {
|
|
5442
|
-
action = JSON.parse(raw);
|
|
5454
|
+
action = normalizeLiveActionContract(JSON.parse(raw));
|
|
5443
5455
|
}
|
|
5444
5456
|
catch { /* Preserve the CLI diagnostic verbatim. */ }
|
|
5445
5457
|
return {
|
|
@@ -5514,6 +5526,11 @@ async function main() {
|
|
|
5514
5526
|
}
|
|
5515
5527
|
};
|
|
5516
5528
|
if (typeof server.registerTool === 'function') {
|
|
5529
|
+
server.registerTool('ductape_doctor', {
|
|
5530
|
+
title: 'Ductape Compatibility Doctor',
|
|
5531
|
+
description: 'Report CLI, SDK, NestJS, MCP, API/schema revision, authentication, workspace, and project linkage facts without exposing credentials.',
|
|
5532
|
+
inputSchema: z.object({}).shape,
|
|
5533
|
+
}, async () => cliHandler({ command: 'doctor --json' }));
|
|
5517
5534
|
server.registerTool('ductape_execute', {
|
|
5518
5535
|
title: 'Ductape SDK Execute',
|
|
5519
5536
|
description: 'Execute a Ductape SDK RUNTIME operation via the backend proxy (publishable key).\n\n' +
|
|
@@ -5757,6 +5774,7 @@ async function main() {
|
|
|
5757
5774
|
}, cliHandler);
|
|
5758
5775
|
}
|
|
5759
5776
|
else if (typeof server.tool === 'function') {
|
|
5777
|
+
server.tool('ductape_doctor', z.object({}).shape, async () => cliHandler({ command: 'doctor --json' }));
|
|
5760
5778
|
server.tool('ductape_execute', executeInputSchema.shape, executeHandler);
|
|
5761
5779
|
server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
|
|
5762
5780
|
server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ductape/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
4
4
|
"description": "MCP server that exposes Ductape SDK operations via the backend proxy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
-
"test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs",
|
|
18
|
+
"test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|