@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.
@@ -0,0 +1,2 @@
1
+ export declare function normalizeLiveActionContract(value: unknown): unknown;
2
+ //# sourceMappingURL=action-contract.d.ts.map
@@ -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, 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?: { notifications?: [{ notification: string, message: string, channels: { email?: { recipients: string[] }, push?: { recipients?: string[] }, sms?: { recipients: string[] } } }], webhooks?: [{ url: string, method?: "GET"|"POST", headers?: object, body?: object }] } }]
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 — rate-limited multi-provider pools:
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 server = new McpServer({ name: 'ductape-mcp', version: '0.1.0' });
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
- if (!isAuthCommand && !isLocalMigrationGuidance) {
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.18",
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
  },