@ductape/mcp 0.2.6 → 0.2.8
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 +8 -1
- package/dist/events-capabilities.d.ts +133 -0
- package/dist/events-capabilities.d.ts.map +1 -0
- package/dist/events-capabilities.js +311 -0
- package/dist/index.js +308 -12
- package/dist/proxy-client.d.ts.map +1 -1
- package/dist/proxy-client.js +90 -5
- package/docs/TOOLS.md +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Use an absolute path for `args[0]`.
|
|
|
55
55
|
|
|
56
56
|
## Tools
|
|
57
57
|
|
|
58
|
-
The server exposes
|
|
58
|
+
The server exposes runtime, schema, documentation, CLI, discovery, migration, and setup tools. Core tools include:
|
|
59
59
|
|
|
60
60
|
1. **`ductape_execute`**:
|
|
61
61
|
- It runs any allowed SDK module method via the backend proxy.
|
|
@@ -82,6 +82,13 @@ The server exposes **three tools**:
|
|
|
82
82
|
- ready-to-copy SDK snippet in `typescript` or `python`
|
|
83
83
|
- Intended for engineers and copilots that need executable examples quickly.
|
|
84
84
|
|
|
85
|
+
4. **`ductape_function_setup`**:
|
|
86
|
+
- Produces the secure local and remote setup for application functions referenced by portable Features.
|
|
87
|
+
- Requires an externally reachable HTTPS base URL (HTTP only for localhost development).
|
|
88
|
+
- Returns deterministic well-known routes, framework raw-body requirements, HMAC-SHA256 headers,
|
|
89
|
+
runtime verification steps, and fail-closed conditions.
|
|
90
|
+
- Agents must implement and verify the route; they must not claim remote availability from local registration alone.
|
|
91
|
+
|
|
85
92
|
The `ductape_cli` MCP tool also exposes public app discovery:
|
|
86
93
|
`marketplace search <capability>`, `marketplace categories`, and
|
|
87
94
|
`marketplace get <app_tag>`. Inspect the app before generating or executing an action payload.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export declare const EVENTS_IMPLEMENTATION_WARNING = "Ductape manages consumer delivery retries and dead-letter queues. Do not create a parallel application transport DLQ. Use an application transactional outbox only to bridge canonical database commits to Ductape publication. Consumers must remain idempotent and replay-safe.";
|
|
2
|
+
export declare const EVENTS_RECOVERY_SEARCH_TERMS: string[];
|
|
3
|
+
export declare const EVENTS_DELIVERY_SEMANTICS: {
|
|
4
|
+
publisherAcceptance: string;
|
|
5
|
+
consumerRetriesManagedBy: string;
|
|
6
|
+
deadLetterQueueManagedBy: string;
|
|
7
|
+
applicationDlqRequired: boolean;
|
|
8
|
+
applicationOutboxRecommended: boolean;
|
|
9
|
+
applicationOutboxPurpose: string;
|
|
10
|
+
orderingGuarantees: string;
|
|
11
|
+
deliveryGuarantee: string;
|
|
12
|
+
deduplicationGuidance: string;
|
|
13
|
+
idempotentConsumerRequired: boolean;
|
|
14
|
+
responsibilities: {
|
|
15
|
+
applicationTransactionalOutbox: string;
|
|
16
|
+
brokerDeliveryRetries: string;
|
|
17
|
+
exhaustedDeliveryDlq: string;
|
|
18
|
+
replayAndReprocessing: string;
|
|
19
|
+
domainRejectedOrInvalidCommands: string;
|
|
20
|
+
consumerMutationIdempotency: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare const EVENTS_PROVIDER_CAPABILITIES: Record<string, Record<string, unknown>>;
|
|
24
|
+
type Operation = {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
inputSchema: Record<string, string>;
|
|
28
|
+
outputSchema: Record<string, string>;
|
|
29
|
+
requiredIdentifiers: string[];
|
|
30
|
+
environmentRequirements: string;
|
|
31
|
+
connectionRequirements: string;
|
|
32
|
+
permissions: string;
|
|
33
|
+
idempotency: string;
|
|
34
|
+
pagination: string;
|
|
35
|
+
applicableStates: string[];
|
|
36
|
+
commonErrors: Array<{
|
|
37
|
+
error: string;
|
|
38
|
+
remediation: string;
|
|
39
|
+
}>;
|
|
40
|
+
mutatesState: boolean;
|
|
41
|
+
mayIncurProviderCost: boolean;
|
|
42
|
+
};
|
|
43
|
+
export declare const EVENTS_OPERATION_REGISTRY: Operation[];
|
|
44
|
+
export declare function eventsCapabilityOverview(): {
|
|
45
|
+
capability: string;
|
|
46
|
+
aliases: string[];
|
|
47
|
+
implementationWarning: string;
|
|
48
|
+
deliverySemantics: {
|
|
49
|
+
publisherAcceptance: string;
|
|
50
|
+
consumerRetriesManagedBy: string;
|
|
51
|
+
deadLetterQueueManagedBy: string;
|
|
52
|
+
applicationDlqRequired: boolean;
|
|
53
|
+
applicationOutboxRecommended: boolean;
|
|
54
|
+
applicationOutboxPurpose: string;
|
|
55
|
+
orderingGuarantees: string;
|
|
56
|
+
deliveryGuarantee: string;
|
|
57
|
+
deduplicationGuidance: string;
|
|
58
|
+
idempotentConsumerRequired: boolean;
|
|
59
|
+
responsibilities: {
|
|
60
|
+
applicationTransactionalOutbox: string;
|
|
61
|
+
brokerDeliveryRetries: string;
|
|
62
|
+
exhaustedDeliveryDlq: string;
|
|
63
|
+
replayAndReprocessing: string;
|
|
64
|
+
domainRejectedOrInvalidCommands: string;
|
|
65
|
+
consumerMutationIdempotency: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
recoverySearchTerms: string[];
|
|
69
|
+
documentationTopics: string[];
|
|
70
|
+
providerCapabilities: Record<string, Record<string, unknown>>;
|
|
71
|
+
operations: Operation[];
|
|
72
|
+
};
|
|
73
|
+
export declare function searchEventsCapabilities(query: string): {
|
|
74
|
+
query: string;
|
|
75
|
+
canonicalTopic: string;
|
|
76
|
+
recoveryMatch: boolean;
|
|
77
|
+
deliverySemantics: {
|
|
78
|
+
publisherAcceptance: string;
|
|
79
|
+
consumerRetriesManagedBy: string;
|
|
80
|
+
deadLetterQueueManagedBy: string;
|
|
81
|
+
applicationDlqRequired: boolean;
|
|
82
|
+
applicationOutboxRecommended: boolean;
|
|
83
|
+
applicationOutboxPurpose: string;
|
|
84
|
+
orderingGuarantees: string;
|
|
85
|
+
deliveryGuarantee: string;
|
|
86
|
+
deduplicationGuidance: string;
|
|
87
|
+
idempotentConsumerRequired: boolean;
|
|
88
|
+
responsibilities: {
|
|
89
|
+
applicationTransactionalOutbox: string;
|
|
90
|
+
brokerDeliveryRetries: string;
|
|
91
|
+
exhaustedDeliveryDlq: string;
|
|
92
|
+
replayAndReprocessing: string;
|
|
93
|
+
domainRejectedOrInvalidCommands: string;
|
|
94
|
+
consumerMutationIdempotency: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
operations: Operation[];
|
|
98
|
+
};
|
|
99
|
+
export declare function inspectEventsComponent(component: any): {
|
|
100
|
+
component: any;
|
|
101
|
+
provider: any;
|
|
102
|
+
environments: any;
|
|
103
|
+
topics: any;
|
|
104
|
+
subscriptions: any;
|
|
105
|
+
consumerBindings: any;
|
|
106
|
+
messageStatistics: string;
|
|
107
|
+
availableOperationalActions: Operation[];
|
|
108
|
+
deliverySemantics: {
|
|
109
|
+
publisherAcceptance: string;
|
|
110
|
+
consumerRetriesManagedBy: string;
|
|
111
|
+
deadLetterQueueManagedBy: string;
|
|
112
|
+
applicationDlqRequired: boolean;
|
|
113
|
+
applicationOutboxRecommended: boolean;
|
|
114
|
+
applicationOutboxPurpose: string;
|
|
115
|
+
orderingGuarantees: string;
|
|
116
|
+
deliveryGuarantee: string;
|
|
117
|
+
deduplicationGuidance: string;
|
|
118
|
+
idempotentConsumerRequired: boolean;
|
|
119
|
+
responsibilities: {
|
|
120
|
+
applicationTransactionalOutbox: string;
|
|
121
|
+
brokerDeliveryRetries: string;
|
|
122
|
+
exhaustedDeliveryDlq: string;
|
|
123
|
+
replayAndReprocessing: string;
|
|
124
|
+
domainRejectedOrInvalidCommands: string;
|
|
125
|
+
consumerMutationIdempotency: string;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
implementationWarning: string;
|
|
129
|
+
configurationWarnings: any;
|
|
130
|
+
documentationTopics: string[];
|
|
131
|
+
};
|
|
132
|
+
export {};
|
|
133
|
+
//# sourceMappingURL=events-capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events-capabilities.d.ts","sourceRoot":"","sources":["../src/events-capabilities.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,6BAA6B,sRAC2O,CAAC;AAEtR,eAAO,MAAM,4BAA4B,UAWxC,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;CAmBrC,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkDhF,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,uBAAuB,EAAE,MAAM,CAAC;IAChC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;CAC/B,CAAC;AAyCF,eAAO,MAAM,yBAAyB,EAAE,SAAS,EA8IhD,CAAC;AAEF,wBAAgB,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWvC;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;EASrD;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCpD"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
export const EVENTS_IMPLEMENTATION_WARNING = 'Ductape manages consumer delivery retries and dead-letter queues. Do not create a parallel application transport DLQ. Use an application transactional outbox only to bridge canonical database commits to Ductape publication. Consumers must remain idempotent and replay-safe.';
|
|
2
|
+
export const EVENTS_RECOVERY_SEARCH_TERMS = [
|
|
3
|
+
'dlq',
|
|
4
|
+
'dead letter',
|
|
5
|
+
'dead letters',
|
|
6
|
+
'failed messages',
|
|
7
|
+
'retry',
|
|
8
|
+
'retries',
|
|
9
|
+
'poison message',
|
|
10
|
+
'replay',
|
|
11
|
+
'reprocess',
|
|
12
|
+
'consumer failure',
|
|
13
|
+
];
|
|
14
|
+
export const EVENTS_DELIVERY_SEMANTICS = {
|
|
15
|
+
publisherAcceptance: 'Ductape acknowledges events accepted for publication; acceptance is not proof of consumer-side mutation.',
|
|
16
|
+
consumerRetriesManagedBy: 'ductape',
|
|
17
|
+
deadLetterQueueManagedBy: 'ductape',
|
|
18
|
+
applicationDlqRequired: false,
|
|
19
|
+
applicationOutboxRecommended: true,
|
|
20
|
+
applicationOutboxPurpose: 'Protect the canonical database transaction-to-Ductape publication boundary.',
|
|
21
|
+
orderingGuarantees: 'Provider and configuration dependent; inspect each environment. Never infer global ordering.',
|
|
22
|
+
deliveryGuarantee: 'At-least-once processing should be assumed unless the inspected provider/configuration explicitly proves otherwise.',
|
|
23
|
+
deduplicationGuidance: 'Use publishIdempotent with a stable logical-operation key where appropriate and deduplicate consumer mutations.',
|
|
24
|
+
idempotentConsumerRequired: true,
|
|
25
|
+
responsibilities: {
|
|
26
|
+
applicationTransactionalOutbox: 'application',
|
|
27
|
+
brokerDeliveryRetries: 'ductape',
|
|
28
|
+
exhaustedDeliveryDlq: 'ductape',
|
|
29
|
+
replayAndReprocessing: 'ductape',
|
|
30
|
+
domainRejectedOrInvalidCommands: 'application (not a transport DLQ)',
|
|
31
|
+
consumerMutationIdempotency: 'application',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export const EVENTS_PROVIDER_CAPABILITIES = {
|
|
35
|
+
google_pubsub: {
|
|
36
|
+
ordering: 'supported only when provider ordering keys/configuration are enabled; not inferred by Ductape',
|
|
37
|
+
acknowledgement: 'provider subscription acknowledgement deadline',
|
|
38
|
+
retriesAndBackoff: 'provider subscription policy; inspect the deployed subscription',
|
|
39
|
+
dlq: 'Ductape recovery operations are available, but provider dead-letter policy must be verified before claiming a provider DLQ exists',
|
|
40
|
+
retention: 'provider topic/subscription retention; inspect live provider configuration',
|
|
41
|
+
},
|
|
42
|
+
aws_sqs: {
|
|
43
|
+
ordering: 'FIFO only for FIFO queues; standard queues do not guarantee strict ordering',
|
|
44
|
+
acknowledgement: 'visibility timeout',
|
|
45
|
+
retriesAndBackoff: 'receive count and visibility timeout/provider redrive policy',
|
|
46
|
+
dlq: 'provider redrive/DLQ must be configured and verified; Ductape exposes tracked dead letters and reprocessing',
|
|
47
|
+
retention: 'provider queue retention',
|
|
48
|
+
},
|
|
49
|
+
azure_servicebus: {
|
|
50
|
+
ordering: 'sessions can provide ordered handling when configured; otherwise provider-controlled',
|
|
51
|
+
acknowledgement: 'message lock duration',
|
|
52
|
+
retriesAndBackoff: 'max delivery count and lock/redelivery behavior',
|
|
53
|
+
dlq: 'provider entity DLQ exists, but its configured state and access must be verified',
|
|
54
|
+
retention: 'provider entity configuration',
|
|
55
|
+
},
|
|
56
|
+
kafka: {
|
|
57
|
+
ordering: 'within a partition only',
|
|
58
|
+
acknowledgement: 'consumer offset commit',
|
|
59
|
+
retriesAndBackoff: 'Ductape consumer failure tracking plus application/provider configuration',
|
|
60
|
+
dlq: 'do not assume a native broker DLQ topic exists unless it is configured and verified',
|
|
61
|
+
retention: 'topic retention configuration',
|
|
62
|
+
},
|
|
63
|
+
rabbitmq: {
|
|
64
|
+
ordering: 'queue ordering can be affected by redelivery and multiple consumers',
|
|
65
|
+
acknowledgement: 'broker acknowledgement/nack',
|
|
66
|
+
retriesAndBackoff: 'redelivery and configured exchange/queue policies',
|
|
67
|
+
dlq: 'dead-letter exchange/queue must be configured and verified before claiming provider DLQ availability',
|
|
68
|
+
retention: 'queue/message TTL configuration',
|
|
69
|
+
},
|
|
70
|
+
redis: {
|
|
71
|
+
ordering: 'provider/mode dependent',
|
|
72
|
+
acknowledgement: 'not uniformly supported across Redis pub/sub and stream modes',
|
|
73
|
+
retriesAndBackoff: 'Ductape tracking; provider-native retry semantics may be unsupported',
|
|
74
|
+
dlq: 'no provider DLQ should be implied without a verified stream/consumer-group recovery configuration',
|
|
75
|
+
retention: 'provider/mode dependent',
|
|
76
|
+
},
|
|
77
|
+
nats: {
|
|
78
|
+
ordering: 'subject/stream configuration dependent',
|
|
79
|
+
acknowledgement: 'JetStream configuration dependent; core NATS has different semantics',
|
|
80
|
+
retriesAndBackoff: 'provider mode/configuration dependent',
|
|
81
|
+
dlq: 'do not imply a provider DLQ without verified JetStream/recovery configuration',
|
|
82
|
+
retention: 'stream configuration dependent',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const runtimeRead = {
|
|
86
|
+
environmentRequirements: 'product environment must exist and the Events component must be configured for it',
|
|
87
|
+
connectionRequirements: 'configured provider connection for the selected Events environment',
|
|
88
|
+
permissions: 'publishable-key runtime access plus product/environment access',
|
|
89
|
+
applicableStates: ['active', 'provider-ready'],
|
|
90
|
+
commonErrors: [
|
|
91
|
+
{ error: 'broker/topic not found', remediation: 'inspect the Events component and use its exact broker/topic tags' },
|
|
92
|
+
{ error: 'environment not configured', remediation: 'configure that environment on the Events component' },
|
|
93
|
+
{ error: 'authentication/permission denied', remediation: 'verify publishable key, workspace, product, and environment access' },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
function adminOperation(name, description, inputSchema, requiredIdentifiers, mutatesState) {
|
|
97
|
+
return {
|
|
98
|
+
name, description, inputSchema,
|
|
99
|
+
outputSchema: mutatesState ? { resource: 'persisted Events component/topic' } : { resource: 'Events component/topic or array' },
|
|
100
|
+
requiredIdentifiers,
|
|
101
|
+
environmentRequirements: 'administrative product access; create/update must cover every product environment where required',
|
|
102
|
+
connectionRequirements: 'provider-specific credentials or a verified cloud connection for every configured environment',
|
|
103
|
+
permissions: 'authenticated access-key CLI session with workspace/product administration permission',
|
|
104
|
+
idempotency: mutatesState ? 'not guaranteed; fetch first and use update rather than duplicate create' : 'read-only',
|
|
105
|
+
pagination: name.endsWith('.list') ? 'provider/component list; pagination is not currently exposed by this admin method' : 'none',
|
|
106
|
+
applicableStates: ['draft', 'active', 'provider-ready'],
|
|
107
|
+
commonErrors: [
|
|
108
|
+
{ error: 'missing environment/provider fields', remediation: 'read the targeted live events.create/events.update schema and cover every product environment' },
|
|
109
|
+
{ error: 'unsupported provider configuration', remediation: 'use the selected env type’s live config branch; do not copy fields between providers' },
|
|
110
|
+
{ error: 'permission denied', remediation: 'authenticate the CLI and select the correct workspace; do not use ductape_execute for admin writes' },
|
|
111
|
+
],
|
|
112
|
+
mutatesState,
|
|
113
|
+
mayIncurProviderCost: mutatesState,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export const EVENTS_OPERATION_REGISTRY = [
|
|
117
|
+
adminOperation('events.create', 'Register an Events component and its per-environment provider connections.', { product: 'string', tag: 'string', name: 'string', description: 'string?', envs: 'provider-specific env[]' }, ['product', 'tag', 'name', 'envs'], true),
|
|
118
|
+
adminOperation('events.update', 'Update an Events component/provider configuration.', { product: 'string', brokerTag: 'string', data: 'targeted live events.update schema' }, ['product', 'brokerTag'], true),
|
|
119
|
+
adminOperation('events.fetch', 'Fetch one Events component.', { product: 'string', brokerTag: 'string' }, ['product', 'brokerTag'], false),
|
|
120
|
+
adminOperation('events.list', 'List Events components for a product.', { product: 'string' }, ['product'], false),
|
|
121
|
+
adminOperation('events.delete', 'Delete or retire an Events component.', { product: 'string', brokerTag: 'string' }, ['product', 'brokerTag'], true),
|
|
122
|
+
adminOperation('events.topics.create', 'Create a topic definition on a registered Events component.', { product: 'string', tag: 'broker:topic', name: 'string', sample: 'object?', idempotent: 'boolean?', queueUrls: 'array?' }, ['product', 'tag', 'name'], true),
|
|
123
|
+
adminOperation('events.topics.update', 'Update an Events topic definition.', { product: 'string', topicTag: 'broker:topic', data: 'topic patch' }, ['product', 'topicTag'], true),
|
|
124
|
+
adminOperation('events.topics.fetch', 'Fetch one Events topic definition.', { product: 'string', topicTag: 'broker:topic' }, ['product', 'topicTag'], false),
|
|
125
|
+
adminOperation('events.topics.list', 'List topic definitions for an Events component.', { product: 'string', brokerTag: 'string' }, ['product', 'brokerTag'], false),
|
|
126
|
+
{
|
|
127
|
+
name: 'events.produce', description: 'Publish an event immediately.',
|
|
128
|
+
inputSchema: { product: 'string', env: 'string', event: 'broker:topic', message: 'object', session: 'string?' },
|
|
129
|
+
outputSchema: { accepted: 'boolean', messageId: 'string?' }, requiredIdentifiers: ['product', 'env', 'event'],
|
|
130
|
+
...runtimeRead, idempotency: 'not inherently idempotent; use events.publishIdempotent when duplicate publication matters',
|
|
131
|
+
pagination: 'none', mutatesState: true, mayIncurProviderCost: true,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'events.publishIdempotent', description: 'Publish with a stable deduplication key.',
|
|
135
|
+
inputSchema: { product: 'string', env: 'string', event: 'broker:topic', message: 'object', idempotencyKey: 'string', idempotencyTtl: 'number?' },
|
|
136
|
+
outputSchema: { accepted: 'boolean', duplicate: 'boolean?', messageId: 'string?' }, requiredIdentifiers: ['product', 'env', 'event', 'idempotencyKey'],
|
|
137
|
+
...runtimeRead, idempotency: 'deduplicates the logical publication for the configured TTL',
|
|
138
|
+
pagination: 'none', mutatesState: true, mayIncurProviderCost: true,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'events.consume', description: 'Register a server-side consumer; successful return acknowledges and thrown errors trigger managed failure handling.',
|
|
142
|
+
inputSchema: { product: 'string', env: 'string', event: 'broker:topic', callback: 'function' },
|
|
143
|
+
outputSchema: { subscribed: 'boolean', consumerTag: 'string?' }, requiredIdentifiers: ['product', 'env', 'event'],
|
|
144
|
+
...runtimeRead, permissions: 'server-side SDK access; browser clients cannot consume',
|
|
145
|
+
idempotency: 'consumer mutations must be idempotent and replay-safe', pagination: 'none',
|
|
146
|
+
mutatesState: true, mayIncurProviderCost: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'events.dispatch', description: 'Queue or schedule an Events publication.',
|
|
150
|
+
inputSchema: { product: 'string', env: 'string', event: 'broker:topic', input: '{message: object}', retries: 'number?', schedule: 'object?' },
|
|
151
|
+
outputSchema: { job_id: 'string', status: 'scheduled|queued', scheduled_at: 'string?', recurring: 'boolean' },
|
|
152
|
+
requiredIdentifiers: ['product', 'env', 'event'], ...runtimeRead,
|
|
153
|
+
connectionRequirements: 'Events provider connection plus redis_url for dispatch scheduling',
|
|
154
|
+
idempotency: 'job/publication idempotency depends on stable job and message keys; consumers remain idempotent',
|
|
155
|
+
pagination: 'none', mutatesState: true, mayIncurProviderCost: true,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'events.messages.query', description: 'Query tracked broker messages and delivery state.',
|
|
159
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topicTag: 'string?', producerTag: 'string?', consumerTag: 'string?', status: 'string?', startDate: 'ISO date?', endDate: 'ISO date?', page: 'number?', limit: 'number?' },
|
|
160
|
+
outputSchema: { messages: 'array', total: 'number', page: 'number', limit: 'number', hasMore: 'boolean' },
|
|
161
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
162
|
+
idempotency: 'read-only', pagination: 'page/limit with total and hasMore', mutatesState: false, mayIncurProviderCost: false,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'events.getEvents', description: 'Query tracked Events records by broker, topic, category, status, idempotency, and date range.',
|
|
166
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topic: 'string?', category: 'string?', status: 'string?', idempotent: 'boolean?', startDate: 'Date?', endDate: 'Date?', page: 'number?', limit: 'number?' },
|
|
167
|
+
outputSchema: { events: 'array', total: 'number', page: 'number', limit: 'number', hasMore: 'boolean' },
|
|
168
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
169
|
+
idempotency: 'read-only', pagination: 'page/limit with total and hasMore',
|
|
170
|
+
mutatesState: false, mayIncurProviderCost: false,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'events.getEvent', description: 'Fetch one tracked Events record by ID.',
|
|
174
|
+
inputSchema: { product: 'string', eventId: 'string' },
|
|
175
|
+
outputSchema: { event: 'tracked event or null' },
|
|
176
|
+
requiredIdentifiers: ['product', 'eventId'], ...runtimeRead,
|
|
177
|
+
environmentRequirements: 'the event ID must belong to the selected product',
|
|
178
|
+
idempotency: 'read-only', pagination: 'none',
|
|
179
|
+
mutatesState: false, mayIncurProviderCost: false,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'events.getEventStats', description: 'Fetch aggregate tracked event statistics.',
|
|
183
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string' },
|
|
184
|
+
outputSchema: { total_events: 'number', failed_count: 'number', success_count: 'number', status_breakdown: 'object?' },
|
|
185
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
186
|
+
idempotency: 'read-only', pagination: 'none',
|
|
187
|
+
mutatesState: false, mayIncurProviderCost: false,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'events.messages.getDeadLetters', description: 'List messages whose managed consumer delivery attempts were exhausted.',
|
|
191
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topicTag: 'string?', consumerTag: 'string?', startDate: 'ISO date?', endDate: 'ISO date?', page: 'number?', limit: 'number?' },
|
|
192
|
+
outputSchema: { deadLetters: 'array', total: 'number', page: 'number', limit: 'number', hasMore: 'boolean' },
|
|
193
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
194
|
+
idempotency: 'read-only', pagination: 'page/limit with total and hasMore', mutatesState: false, mayIncurProviderCost: false,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'events.reprocessDLQ', description: 'Requeue selected or bounded dead-lettered messages for Ductape-managed delivery.',
|
|
198
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topicTag: 'string?', messageIds: 'string[]?', limit: 'number?' },
|
|
199
|
+
outputSchema: { reprocessed: 'number', failed: 'number', messageIds: 'string[]?' },
|
|
200
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
201
|
+
permissions: 'publishable-key runtime mutation access plus product/environment access',
|
|
202
|
+
idempotency: 'may redeliver; consumer mutation must be idempotent', pagination: 'bounded by messageIds or limit, not page-based',
|
|
203
|
+
applicableStates: ['dead-lettered', 'provider-ready'], mutatesState: true, mayIncurProviderCost: true,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'events.replayEvent', description: 'Replay one tracked event by event identifier.',
|
|
207
|
+
inputSchema: { product: 'string', env: 'string', eventId: 'string', force: 'boolean?' },
|
|
208
|
+
outputSchema: { replayed: 'boolean', eventId: 'string', messageId: 'string?' },
|
|
209
|
+
requiredIdentifiers: ['product', 'env', 'eventId'], ...runtimeRead,
|
|
210
|
+
permissions: 'publishable-key runtime mutation access plus product/environment access',
|
|
211
|
+
idempotency: 'replay intentionally redelivers; force may bypass replay guards; consumer mutation must be idempotent',
|
|
212
|
+
pagination: 'none', applicableStates: ['tracked', 'failed', 'dead-lettered'],
|
|
213
|
+
mutatesState: true, mayIncurProviderCost: true,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'events.messages.getStats', description: 'Return aggregate delivery and failure statistics.',
|
|
217
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string' },
|
|
218
|
+
outputSchema: { totals: 'object', delivery: 'object', failures: 'object' },
|
|
219
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
220
|
+
idempotency: 'read-only', pagination: 'none', mutatesState: false, mayIncurProviderCost: false,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'events.messages.getDashboard', description: 'Return operational overview, recent messages, and recovery indicators.',
|
|
224
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string' },
|
|
225
|
+
outputSchema: { overview: 'object', recent_messages: 'array', health: 'object?' },
|
|
226
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
227
|
+
idempotency: 'read-only', pagination: 'fixed dashboard window', mutatesState: false, mayIncurProviderCost: false,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'events.messages.getProducers', description: 'List observed producers.',
|
|
231
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topicTag: 'string?', page: 'number?', limit: 'number?' },
|
|
232
|
+
outputSchema: { producers: 'array', total: 'number', page: 'number', limit: 'number', hasMore: 'boolean' },
|
|
233
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
234
|
+
idempotency: 'read-only', pagination: 'page/limit', mutatesState: false, mayIncurProviderCost: false,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'events.messages.getConsumers', description: 'List observed consumers and bindings.',
|
|
238
|
+
inputSchema: { product: 'string', env: 'string', brokerTag: 'string', topicTag: 'string?', page: 'number?', limit: 'number?' },
|
|
239
|
+
outputSchema: { consumers: 'array', total: 'number', page: 'number', limit: 'number', hasMore: 'boolean' },
|
|
240
|
+
requiredIdentifiers: ['product', 'env', 'brokerTag'], ...runtimeRead,
|
|
241
|
+
idempotency: 'read-only', pagination: 'page/limit', mutatesState: false, mayIncurProviderCost: false,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'events.checkIdempotency', description: 'Check whether a publication idempotency key has already been accepted.',
|
|
245
|
+
inputSchema: { product: 'string', env: 'string', idempotencyKey: 'string' },
|
|
246
|
+
outputSchema: { exists: 'boolean', event_id: 'string?' }, requiredIdentifiers: ['product', 'env', 'idempotencyKey'],
|
|
247
|
+
...runtimeRead, idempotency: 'read-only idempotency lookup', pagination: 'none',
|
|
248
|
+
mutatesState: false, mayIncurProviderCost: false,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'events.testConnection', description: 'Test the configured provider connection for one Events environment.',
|
|
252
|
+
inputSchema: { product: 'string', env: 'string', broker: 'string' },
|
|
253
|
+
outputSchema: { connected: 'boolean', latency: 'number?', error: 'string?' },
|
|
254
|
+
requiredIdentifiers: ['product', 'env', 'broker'], ...runtimeRead,
|
|
255
|
+
idempotency: 'read-only connection probe', pagination: 'none',
|
|
256
|
+
mutatesState: false, mayIncurProviderCost: false,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
export function eventsCapabilityOverview() {
|
|
260
|
+
return {
|
|
261
|
+
capability: 'events',
|
|
262
|
+
aliases: ['messageBrokers', 'brokers', 'messaging'],
|
|
263
|
+
implementationWarning: EVENTS_IMPLEMENTATION_WARNING,
|
|
264
|
+
deliverySemantics: EVENTS_DELIVERY_SEMANTICS,
|
|
265
|
+
recoverySearchTerms: EVENTS_RECOVERY_SEARCH_TERMS,
|
|
266
|
+
documentationTopics: ['events', 'resilience'],
|
|
267
|
+
providerCapabilities: EVENTS_PROVIDER_CAPABILITIES,
|
|
268
|
+
operations: EVENTS_OPERATION_REGISTRY,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
export function searchEventsCapabilities(query) {
|
|
272
|
+
const normalized = query.toLowerCase();
|
|
273
|
+
const recoveryMatch = EVENTS_RECOVERY_SEARCH_TERMS.some(term => normalized.includes(term));
|
|
274
|
+
const operations = EVENTS_OPERATION_REGISTRY.filter(operation => recoveryMatch
|
|
275
|
+
? /deadletter|reprocess|replay|query|getstats|getdashboard/i.test(operation.name)
|
|
276
|
+
: `${operation.name} ${operation.description}`.toLowerCase().includes(normalized));
|
|
277
|
+
return { query, canonicalTopic: 'events', recoveryMatch, deliverySemantics: EVENTS_DELIVERY_SEMANTICS, operations };
|
|
278
|
+
}
|
|
279
|
+
export function inspectEventsComponent(component) {
|
|
280
|
+
const environments = (component?.envs || []).map((env) => ({
|
|
281
|
+
slug: env.slug ?? env.env_slug ?? null,
|
|
282
|
+
provider: env.type ?? component.type ?? null,
|
|
283
|
+
connection: env.cloud ?? env.instance ?? (env.config ? 'configured' : null),
|
|
284
|
+
retryPolicy: env.retryPolicy ?? component.retryPolicy ?? 'provider-default',
|
|
285
|
+
maximumDeliveryAttempts: env.maxDeliveryAttempts ?? component.maxDeliveryAttempts ?? 'provider-controlled-or-unverified',
|
|
286
|
+
acknowledgementDeadline: env.acknowledgementDeadline ?? component.acknowledgementDeadline ?? 'provider-controlled-or-unverified',
|
|
287
|
+
backoff: env.backoff ?? component.backoff ?? 'provider-controlled-or-unverified',
|
|
288
|
+
dlq: env.dlq ?? component.dlq ?? { configured: 'unverified', managedBy: 'ductape' },
|
|
289
|
+
retention: env.retention ?? component.retention ?? 'provider-controlled-or-unverified',
|
|
290
|
+
ordering: env.ordering ?? component.ordering ?? 'provider-controlled-or-unverified',
|
|
291
|
+
filters: env.filters ?? component.filters ?? 'none-configured-or-unverified',
|
|
292
|
+
providerCapabilities: EVENTS_PROVIDER_CAPABILITIES[env.type ?? component.type] ??
|
|
293
|
+
{ status: 'unsupported-or-unknown-provider; inspect live provider configuration' },
|
|
294
|
+
}));
|
|
295
|
+
return {
|
|
296
|
+
component,
|
|
297
|
+
provider: component?.type ?? 'environment-specific',
|
|
298
|
+
environments,
|
|
299
|
+
topics: component?.topics ?? [],
|
|
300
|
+
subscriptions: component?.subscriptions ?? 'provider-controlled-or-unverified',
|
|
301
|
+
consumerBindings: component?.consumers ?? [],
|
|
302
|
+
messageStatistics: 'Call events.messages.getStats or events.messages.getDashboard with product, env, and brokerTag.',
|
|
303
|
+
availableOperationalActions: EVENTS_OPERATION_REGISTRY,
|
|
304
|
+
deliverySemantics: EVENTS_DELIVERY_SEMANTICS,
|
|
305
|
+
implementationWarning: EVENTS_IMPLEMENTATION_WARNING,
|
|
306
|
+
configurationWarnings: environments
|
|
307
|
+
.filter((env) => env.retryPolicy === 'provider-default' || env.dlq?.configured === 'unverified')
|
|
308
|
+
.map((env) => `${env.slug ?? 'unknown env'}: retry/DLQ configuration is not verified; provider defaults may apply.`),
|
|
309
|
+
documentationTopics: ['events', 'resilience'],
|
|
310
|
+
};
|
|
311
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { homedir } from 'os';
|
|
|
15
15
|
import { delimiter, join } from 'path';
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { executeViaProxy, generateExecutablePayload, getAssetSchemas, } from './proxy-client.js';
|
|
18
|
+
import { EVENTS_DELIVERY_SEMANTICS, EVENTS_IMPLEMENTATION_WARNING, eventsCapabilityOverview, inspectEventsComponent, searchEventsCapabilities, } from './events-capabilities.js';
|
|
18
19
|
const MODULES = [
|
|
19
20
|
'product', 'app', 'databases', 'graph', 'webhooks', 'notifications',
|
|
20
21
|
'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
|
|
@@ -556,6 +557,14 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
556
557
|
events.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, consumerTag?, startDate?, endDate?, page?, limit? }]
|
|
557
558
|
events.messages.getStats [{ product, env, brokerTag }]
|
|
558
559
|
events.messages.getDashboard [{ product, env, brokerTag }]
|
|
560
|
+
events.getEvents [{ product, env, brokerTag, topic?, category?, status?, idempotent?, startDate?, endDate?, page?, limit? }]
|
|
561
|
+
events.getEvent [{ product, eventId }]
|
|
562
|
+
events.getEventStats [{ product, env, brokerTag }]
|
|
563
|
+
events.replayEvent [{ product, env, eventId, force? }]
|
|
564
|
+
events.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
565
|
+
events.publishIdempotent [{ product, env, event, message, idempotencyKey, idempotencyTtl? }]
|
|
566
|
+
events.checkIdempotency [{ product, env, idempotencyKey }]
|
|
567
|
+
events.testConnection [{ product, env, broker }]
|
|
559
568
|
|
|
560
569
|
━━━ MODULE: storage ━━━
|
|
561
570
|
storage.create [{ product: string, tag: string, name: string, description?: string, envs: [{ slug: string, type: "aws"|"azure"|"gcp", config: { bucket?: string, region?: string, accessKeyId?: string, secretAccessKey?: string, containerName?: string, connectionString?: string, projectId?: string, keyFilename?: string } }] }]
|
|
@@ -1340,7 +1349,19 @@ const docsInputSchema = z.object({
|
|
|
1340
1349
|
topic: z.string().describe('Feature topic to look up. Supported: ' +
|
|
1341
1350
|
'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
1342
1351
|
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
1343
|
-
'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
|
|
1352
|
+
'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
|
|
1353
|
+
});
|
|
1354
|
+
const eventsDiscoveryInputSchema = z.object({
|
|
1355
|
+
query: z.string().optional().describe('Capability or recovery search, including DLQ, dead letter, failed messages, retry, poison message, replay, or consumer failure.'),
|
|
1356
|
+
product: z.string().optional().describe('Product tag used for live component inspection.'),
|
|
1357
|
+
component: z.string().optional().describe('Events component/broker tag used for live inspection.'),
|
|
1358
|
+
});
|
|
1359
|
+
const portableFunctionSetupInputSchema = z.object({
|
|
1360
|
+
framework: z.enum(['express', 'nestjs', 'fastify', 'other']).describe('Application HTTP framework.'),
|
|
1361
|
+
base_url: z.string().url().describe('Externally reachable HTTPS origin. HTTP is accepted only for localhost development.'),
|
|
1362
|
+
namespace: z.string().min(1).describe('Portable function namespace.'),
|
|
1363
|
+
version: z.string().min(1).describe('Contract version.'),
|
|
1364
|
+
operations: z.array(z.string().min(1)).min(1).describe('Operation names that must be registered and exposed.'),
|
|
1344
1365
|
});
|
|
1345
1366
|
const migrationInputSchema = z.object({
|
|
1346
1367
|
source: z.string().describe('Absolute path to the existing codebase.'),
|
|
@@ -1357,6 +1378,65 @@ const migrationInputSchema = z.object({
|
|
|
1357
1378
|
write: z.boolean().optional().default(false).describe('Write redacted advisory artifacts only. This never writes application code or executable Ductape assets.'),
|
|
1358
1379
|
});
|
|
1359
1380
|
const DOCS = {
|
|
1381
|
+
'portable-functions': `
|
|
1382
|
+
PORTABLE APPLICATION FUNCTIONS
|
|
1383
|
+
|
|
1384
|
+
Use portable functions when a Feature needs application-owned logic that cannot be expressed with
|
|
1385
|
+
database, action, Event, storage, graph, vector, session, quota, fallback, or transform primitives.
|
|
1386
|
+
|
|
1387
|
+
Non-negotiable rule: arbitrary JavaScript/TypeScript callbacks are not serializable. Never write
|
|
1388
|
+
ctx.step('x', () => applicationService.method()) and assume the method will run elsewhere. The
|
|
1389
|
+
Feature compiler must reject a step that records no portable operation.
|
|
1390
|
+
|
|
1391
|
+
Canonical TypeScript pattern:
|
|
1392
|
+
const AuthFunctions = defineFunctions({
|
|
1393
|
+
namespace: 'statecraft-auth', version: '1',
|
|
1394
|
+
operations: {
|
|
1395
|
+
register: {
|
|
1396
|
+
input: { type: 'object', required: ['email', 'password'], properties: {
|
|
1397
|
+
email: { type: 'string' }, password: { type: 'string', minLength: 12 }
|
|
1398
|
+
}, additionalProperties: false },
|
|
1399
|
+
output: { type: 'object', required: ['token', 'player'], properties: {
|
|
1400
|
+
token: { type: 'string' }, player: { type: 'object' }
|
|
1401
|
+
} },
|
|
1402
|
+
transports: [{ type: 'local' }],
|
|
1403
|
+
handler: (input, context) => authService.register(input.email, input.password)
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
ductape.functions.register(AuthFunctions);
|
|
1408
|
+
await ductape.feature.define({
|
|
1409
|
+
tag: 'register-player', name: 'Register Player',
|
|
1410
|
+
input: { email: { type: 'string', required: true }, password: { type: 'string', required: true } },
|
|
1411
|
+
handler: async ctx => {
|
|
1412
|
+
const auth = ctx.functions.use(AuthFunctions);
|
|
1413
|
+
return ctx.step('register', () => auth.register({ email: ctx.input.email, password: ctx.input.password }));
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
The compiled step stores namespace, operation, version, input/output JSON Schemas, timeout,
|
|
1418
|
+
idempotency declaration, and permitted transports. It never stores the handler or a sample result.
|
|
1419
|
+
|
|
1420
|
+
Resolution order is local registered handler, then a contract-declared signed HTTP transport.
|
|
1421
|
+
HTTP function calls use Ductape HMAC-SHA256 headers. Raw arbitrary URLs and unsigned invocation are
|
|
1422
|
+
not supported. Application endpoints should use handlePortableFunctionHttpRequest and must receive
|
|
1423
|
+
the raw request body so signature verification covers exactly the bytes sent.
|
|
1424
|
+
|
|
1425
|
+
The feature session is inherited automatically in invocation.context.session, not mixed into the
|
|
1426
|
+
business input. Invocation context also includes product, env, workspace_id, feature_id,
|
|
1427
|
+
feature_tag, step_tag, invocation_id, and deadline_at.
|
|
1428
|
+
|
|
1429
|
+
Failure behavior is strict:
|
|
1430
|
+
FUNCTION_UNAVAILABLE no local handler or configured HTTP transport
|
|
1431
|
+
FUNCTION_SCHEMA_VALIDATION_FAILED input or output violates the contract
|
|
1432
|
+
FUNCTION_SIGNATURE_INVALID HTTP signature is absent, invalid, or expired
|
|
1433
|
+
FUNCTION_CORRELATION_MISMATCH response invocation_id differs from request
|
|
1434
|
+
FUNCTION_TIMEOUT operation exceeded its declared deadline
|
|
1435
|
+
|
|
1436
|
+
Do not invent a function contract. Inspect the application's defineFunctions declarations and use
|
|
1437
|
+
the exact namespace, operation, version, schemas, and transports. If no contract exists, add one in
|
|
1438
|
+
application code and explicitly register its runtime implementation.
|
|
1439
|
+
`.trim(),
|
|
1360
1440
|
migration: `
|
|
1361
1441
|
DUCTAPE CODEBASE MIGRATION GUIDE
|
|
1362
1442
|
|
|
@@ -2807,6 +2887,13 @@ DUCTAPE RESILIENCE
|
|
|
2807
2887
|
Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
|
|
2808
2888
|
provider switching), and healthchecks (continuous probe monitoring with failure actions).
|
|
2809
2889
|
|
|
2890
|
+
EVENTS DELIVERY BOUNDARY
|
|
2891
|
+
Events transport retries, exhausted-delivery DLQs, replay, and reprocessing are Ductape
|
|
2892
|
+
responsibilities. Applications own transactional outboxes at the database-to-publication
|
|
2893
|
+
boundary, domain rejection records, and idempotent/replay-safe consumer mutations. Never advise
|
|
2894
|
+
a parallel application transport DLQ. See ductape_docs({ topic: "events" }) and
|
|
2895
|
+
ductape_events_discover({ query: "consumer failure" }).
|
|
2896
|
+
|
|
2810
2897
|
CONFIGURATION BOUNDARY
|
|
2811
2898
|
Quotas, fallbacks, and health checks are administrative product configuration. Manage them with
|
|
2812
2899
|
the authenticated CLI:
|
|
@@ -3263,7 +3350,8 @@ STEP 2 — INVENTORY existing Ductape components
|
|
|
3263
3350
|
STEP 3 — PLAN each step
|
|
3264
3351
|
For every logical step:
|
|
3265
3352
|
a. Identify whether it is local domain logic, a child Feature, or an existing Ductape component.
|
|
3266
|
-
|
|
3353
|
+
Application-owned logic must be a registered portable function called through ctx.functions.
|
|
3354
|
+
A plain callback that calls an application service is not serializable and must fail compilation.
|
|
3267
3355
|
If a step calls an external service, it MUST go through a registered Ductape App.
|
|
3268
3356
|
If no App for that service exists in the product → mark it "App to create: <service name>".
|
|
3269
3357
|
DO NOT plan a raw HTTP call, a direct dispatch to a URL, or any workaround in place of a missing App.
|
|
@@ -3271,6 +3359,14 @@ STEP 3 — PLAN each step
|
|
|
3271
3359
|
c. Decide if a rollback handler is needed (e.g. charge → refund on later failure).
|
|
3272
3360
|
Rollback is optional and is not a Feature qualification requirement.
|
|
3273
3361
|
d. Decide allow_fail: true for non-critical steps (email, analytics, audit logs)
|
|
3362
|
+
e. If the step needs application-owned code, search the repository for an existing
|
|
3363
|
+
defineFunctions contract and runtime registration. If none exists, design the exact contract.
|
|
3364
|
+
Then ALWAYS call ductape_function_setup with the detected framework, namespace, version,
|
|
3365
|
+
operations, and intended external base URL. Proactively inspect deployment configuration to
|
|
3366
|
+
discover that URL and implement both local registration and the signed HTTPS adapter. If no
|
|
3367
|
+
deployable HTTPS origin exists, report that explicit blocker instead of stopping at a local-only
|
|
3368
|
+
setup. Do not mark the function remotely available until the
|
|
3369
|
+
well-known route is reachable and rejects an invalid signature with HTTP 401.
|
|
3274
3370
|
|
|
3275
3371
|
STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating anything
|
|
3276
3372
|
Show the user:
|
|
@@ -3323,7 +3419,7 @@ STEP 8 — SET rollbacks for reversible steps
|
|
|
3323
3419
|
async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
|
|
3324
3420
|
);
|
|
3325
3421
|
|
|
3326
|
-
Step types:
|
|
3422
|
+
Step types: function | action | database | graph | notification | storage | produce | quota |
|
|
3327
3423
|
fallback | vector | child_feature | sleep | wait_for_signal | checkpoint
|
|
3328
3424
|
|
|
3329
3425
|
Valid synchronous Feature candidates include generate-world, resolve-nation-turn,
|
|
@@ -3429,10 +3525,12 @@ When you call features.define({ handler }), the handler runs TWICE:
|
|
|
3429
3525
|
outer handler body. Code in the outer body runs during recording with proxy values and
|
|
3430
3526
|
may behave unexpectedly (e.g. typeof proxy === 'object' is true but .someField is a proxy).
|
|
3431
3527
|
|
|
3432
|
-
A ctx.step callback
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3528
|
+
A ctx.step callback MUST NOT close over ordinary local domain functions or injected/application
|
|
3529
|
+
services. Closures are not portable and compilation fails closed. Define application-owned logic
|
|
3530
|
+
with defineFunctions, register its local runtime handler, and invoke it through ctx.functions.
|
|
3531
|
+
The MCP workflow must also attempt secure remote availability: detect the framework and deployment
|
|
3532
|
+
origin, install the signed well-known HTTPS adapter, and verify it. Local registration alone is
|
|
3533
|
+
insufficient for a Feature intended to execute outside the application process.
|
|
3436
3534
|
|
|
3437
3535
|
Use an Event when the operation genuinely crosses an asynchronous process/service boundary,
|
|
3438
3536
|
needs broker delivery semantics, or must be consumed independently. Do not produce an Event merely
|
|
@@ -3471,6 +3569,19 @@ When you call features.define({ handler }), the handler runs TWICE:
|
|
|
3471
3569
|
events: `
|
|
3472
3570
|
DUCTAPE EVENTS (MESSAGE BROKERS)
|
|
3473
3571
|
|
|
3572
|
+
RELIABILITY OWNERSHIP — DO NOT DUPLICATE THE TRANSPORT DLQ
|
|
3573
|
+
Ductape owns broker delivery retries, exhausted-delivery dead-letter handling, replay,
|
|
3574
|
+
reprocessing, and message observability. Do not build a parallel MongoDB/application transport
|
|
3575
|
+
DLQ. The application may use a transactional outbox only to bridge its canonical database commit
|
|
3576
|
+
to Ductape publication. Domain-invalid or rejected commands remain application records and are
|
|
3577
|
+
not transport dead letters. Every consumer mutation must be idempotent and replay-safe.
|
|
3578
|
+
Recovery operations: events.messages.getDeadLetters, events.reprocessDLQ, events.replayEvent,
|
|
3579
|
+
events.messages.query, events.messages.getStats, and events.messages.getDashboard.
|
|
3580
|
+
Structured contracts and live component inspection:
|
|
3581
|
+
ductape_events_discover({ query: "DLQ" })
|
|
3582
|
+
ductape_events_discover({ product: "<product-tag>", component: "<events-tag>" })
|
|
3583
|
+
Resilience boundary reference: ductape_docs({ topic: "resilience" }).
|
|
3584
|
+
|
|
3474
3585
|
ARCHITECTURE — always two separate steps:
|
|
3475
3586
|
Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
|
|
3476
3587
|
The broker's envs[] holds connection credentials and host/project info, NOT topics.
|
|
@@ -3775,6 +3886,9 @@ Import (register an EXISTING cloud resource):
|
|
|
3775
3886
|
events.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, consumerTag?, limit? }]
|
|
3776
3887
|
events.messages.getProducers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
3777
3888
|
events.messages.getConsumers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
3889
|
+
events.getEvents [{ product, env, brokerTag, topic?, category?, status?, idempotent?, startDate?, endDate?, page?, limit? }]
|
|
3890
|
+
events.getEvent [{ product, eventId }]
|
|
3891
|
+
events.getEventStats [{ product, env, brokerTag }]
|
|
3778
3892
|
events.replayEvent [{ product, env, eventId, force? }]
|
|
3779
3893
|
events.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
3780
3894
|
events.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
@@ -4371,7 +4485,9 @@ DEPRECATED ALIASES
|
|
|
4371
4485
|
`.trim(),
|
|
4372
4486
|
};
|
|
4373
4487
|
const docsHandler = async (args) => {
|
|
4374
|
-
const
|
|
4488
|
+
const requested = args.topic.toLowerCase().trim();
|
|
4489
|
+
const recoveryAliases = /^(dlq|dead[- ]?letters?|failed messages?|retry|retries|poison messages?|replay|reprocess|consumer failures?)$/;
|
|
4490
|
+
const key = recoveryAliases.test(requested) ? 'events' : requested;
|
|
4375
4491
|
const doc = DOCS[key];
|
|
4376
4492
|
if (!doc) {
|
|
4377
4493
|
const available = Object.keys(DOCS).join(', ');
|
|
@@ -4379,7 +4495,67 @@ const docsHandler = async (args) => {
|
|
|
4379
4495
|
content: [{ type: 'text', text: `Unknown topic "${args.topic}". Available topics: ${available}` }],
|
|
4380
4496
|
};
|
|
4381
4497
|
}
|
|
4382
|
-
|
|
4498
|
+
const structured = key === 'events'
|
|
4499
|
+
? `\n\nSTRUCTURED EVENTS CAPABILITY INDEX\n${JSON.stringify(eventsCapabilityOverview(), null, 2)}`
|
|
4500
|
+
: key === 'resilience'
|
|
4501
|
+
? `\n\nEVENTS DELIVERY RESPONSIBILITY BOUNDARY\n${JSON.stringify({
|
|
4502
|
+
implementationWarning: EVENTS_IMPLEMENTATION_WARNING,
|
|
4503
|
+
deliverySemantics: EVENTS_DELIVERY_SEMANTICS,
|
|
4504
|
+
canonicalDocumentationTopic: 'events',
|
|
4505
|
+
}, null, 2)}`
|
|
4506
|
+
: '';
|
|
4507
|
+
return { content: [{ type: 'text', text: doc + structured }] };
|
|
4508
|
+
};
|
|
4509
|
+
const portableFunctionSetupHandler = async (args) => {
|
|
4510
|
+
const parsed = new URL(args.base_url);
|
|
4511
|
+
const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
|
|
4512
|
+
if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
|
|
4513
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
4514
|
+
ok: false,
|
|
4515
|
+
error: 'FUNCTION_INSECURE_TRANSPORT',
|
|
4516
|
+
message: 'Use HTTPS for portable function endpoints. HTTP is allowed only for localhost development.',
|
|
4517
|
+
}, null, 2) }], isError: true };
|
|
4518
|
+
}
|
|
4519
|
+
const base = args.base_url.replace(/\/$/, '');
|
|
4520
|
+
const routes = args.operations.map(operation => ({
|
|
4521
|
+
operation,
|
|
4522
|
+
url: `${base}/.well-known/ductape/functions/${encodeURIComponent(args.namespace)}/${encodeURIComponent(args.version)}/${encodeURIComponent(operation)}`,
|
|
4523
|
+
}));
|
|
4524
|
+
const adapter = args.framework === 'nestjs'
|
|
4525
|
+
? `Configure rawBody: true in NestFactory.create, then add a POST controller route at\n` +
|
|
4526
|
+
`/.well-known/ductape/functions/:namespace/:version/:operation. Pass request.rawBody, headers,\n` +
|
|
4527
|
+
`and params to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
|
|
4528
|
+
: args.framework === 'fastify'
|
|
4529
|
+
? `Register a raw-body plugin for only the well-known function route, then pass rawBody, headers,\nparams to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
|
|
4530
|
+
: args.framework === 'express'
|
|
4531
|
+
? `Mount express.raw({ type: 'application/json' }) before JSON parsing on the well-known function route,\nthen pass request.body, headers, and params to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
|
|
4532
|
+
: `Preserve the exact raw JSON request bytes and pass body, headers, and route params to\nhandlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`;
|
|
4533
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
4534
|
+
ok: true,
|
|
4535
|
+
requiredActions: [
|
|
4536
|
+
'Create or locate one defineFunctions contract with exact input/output JSON Schemas.',
|
|
4537
|
+
'Register handlers with ductape.functions.register during application startup.',
|
|
4538
|
+
`Set DUCTAPE_FUNCTION_BASE_URL=${JSON.stringify(base)} in the application runtime.`,
|
|
4539
|
+
'Mount the signed well-known HTTP adapter and preserve the exact raw body.',
|
|
4540
|
+
'Do not add bearer keys, access keys, static headers, or unsigned routes to Feature JSON.',
|
|
4541
|
+
'Verify invalid signatures receive HTTP 401 and missing handlers receive HTTP 503.',
|
|
4542
|
+
'Execute each operation and verify actual business state plus Feature/step processor records.',
|
|
4543
|
+
],
|
|
4544
|
+
routes,
|
|
4545
|
+
adapter,
|
|
4546
|
+
security: {
|
|
4547
|
+
scheme: 'HMAC-SHA256',
|
|
4548
|
+
signedValue: '<timestamp>.<raw-request-body>',
|
|
4549
|
+
headers: ['X-Ductape-Invocation-Id', 'X-Ductape-Timestamp', 'X-Ductape-Signature', 'X-Ductape-Function'],
|
|
4550
|
+
replayToleranceMs: 300000,
|
|
4551
|
+
httpsRequired: !local,
|
|
4552
|
+
},
|
|
4553
|
+
stopConditions: [
|
|
4554
|
+
'Do not claim remote availability until the route is reachable from the intended executor.',
|
|
4555
|
+
'Do not compile a closure-only ctx.step; use ctx.functions.',
|
|
4556
|
+
'Do not fall back to sample output when a runtime is unavailable.',
|
|
4557
|
+
],
|
|
4558
|
+
}, null, 2) }] };
|
|
4383
4559
|
};
|
|
4384
4560
|
const cliInputSchema = z.object({
|
|
4385
4561
|
command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
@@ -4528,6 +4704,74 @@ async function main() {
|
|
|
4528
4704
|
...(result.success ? {} : { isError: true }),
|
|
4529
4705
|
};
|
|
4530
4706
|
};
|
|
4707
|
+
const eventsDiscoveryHandler = async (args) => {
|
|
4708
|
+
try {
|
|
4709
|
+
if (args.product || args.component) {
|
|
4710
|
+
if (!args.product || !args.component) {
|
|
4711
|
+
throw new Error('Both product and component are required for live Events inspection.');
|
|
4712
|
+
}
|
|
4713
|
+
const result = await cliHandler({
|
|
4714
|
+
command: `products components get --product-tag ${shellArgument(args.product)} --type events --json`,
|
|
4715
|
+
});
|
|
4716
|
+
if (result.isError)
|
|
4717
|
+
return result;
|
|
4718
|
+
const raw = result.content?.[0]?.text ?? '';
|
|
4719
|
+
let parsed;
|
|
4720
|
+
try {
|
|
4721
|
+
parsed = JSON.parse(raw);
|
|
4722
|
+
}
|
|
4723
|
+
catch {
|
|
4724
|
+
throw new Error(`Events inspection did not return JSON. ${raw}`);
|
|
4725
|
+
}
|
|
4726
|
+
const candidates = Array.isArray(parsed)
|
|
4727
|
+
? parsed
|
|
4728
|
+
: Array.isArray(parsed?.events)
|
|
4729
|
+
? parsed.events
|
|
4730
|
+
: Array.isArray(parsed?.messageBrokers)
|
|
4731
|
+
? parsed.messageBrokers
|
|
4732
|
+
: [parsed];
|
|
4733
|
+
const component = candidates.find((item) => item?.tag === args.component || item?._id === args.component);
|
|
4734
|
+
if (!component) {
|
|
4735
|
+
throw new Error(`Events component "${args.component}" was not found in product "${args.product}". ` +
|
|
4736
|
+
'Use the exact component tag or ID returned by products components get --type events.');
|
|
4737
|
+
}
|
|
4738
|
+
const inspection = inspectEventsComponent(component);
|
|
4739
|
+
const key = process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
4740
|
+
if (key) {
|
|
4741
|
+
inspection.messageStatistics = {};
|
|
4742
|
+
for (const environment of inspection.environments) {
|
|
4743
|
+
if (!environment.slug)
|
|
4744
|
+
continue;
|
|
4745
|
+
try {
|
|
4746
|
+
inspection.messageStatistics[environment.slug] = await executeViaProxy(key, 'events', 'messages.getStats', [{ product: args.product, env: environment.slug, brokerTag: component.tag }]);
|
|
4747
|
+
}
|
|
4748
|
+
catch (error) {
|
|
4749
|
+
inspection.messageStatistics[environment.slug] = {
|
|
4750
|
+
status: 'unavailable',
|
|
4751
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4752
|
+
remediation: 'Verify publishable-key permission, environment configuration, provider readiness, and the exact broker tag.',
|
|
4753
|
+
};
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
return {
|
|
4758
|
+
content: [{ type: 'text', text: JSON.stringify(inspection, null, 2) }],
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
const data = args.query ? searchEventsCapabilities(args.query) : eventsCapabilityOverview();
|
|
4762
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
4763
|
+
}
|
|
4764
|
+
catch (err) {
|
|
4765
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4766
|
+
return {
|
|
4767
|
+
content: [{
|
|
4768
|
+
type: 'text',
|
|
4769
|
+
text: `Error: ${message}\nRemediation: verify product/component identifiers, CLI authentication, workspace selection, and environment/provider configuration.`,
|
|
4770
|
+
}],
|
|
4771
|
+
isError: true,
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4531
4775
|
const migrationHandler = async (args) => {
|
|
4532
4776
|
if (args.mode === 'new-codebase' && !args.destination) {
|
|
4533
4777
|
return {
|
|
@@ -4687,7 +4931,13 @@ async function main() {
|
|
|
4687
4931
|
if (args.method && !args.module) {
|
|
4688
4932
|
throw new Error('module is required when method is provided');
|
|
4689
4933
|
}
|
|
4690
|
-
const
|
|
4934
|
+
const requestedMethod = args.method;
|
|
4935
|
+
const schemaMethod = requestedMethod === 'events.create'
|
|
4936
|
+
? 'messageBrokers.create'
|
|
4937
|
+
: requestedMethod === 'events.update'
|
|
4938
|
+
? 'messageBrokers.update'
|
|
4939
|
+
: requestedMethod;
|
|
4940
|
+
const data = await getAssetSchemas(args.module, schemaMethod);
|
|
4691
4941
|
const compactData = args.module &&
|
|
4692
4942
|
!args.method &&
|
|
4693
4943
|
data &&
|
|
@@ -4700,7 +4950,36 @@ async function main() {
|
|
|
4700
4950
|
hint: `Call ductape_schema with module="${args.module}" and one method for its complete field schema`,
|
|
4701
4951
|
}
|
|
4702
4952
|
: data;
|
|
4703
|
-
|
|
4953
|
+
const isEventsSchema = args.module === 'product' &&
|
|
4954
|
+
Boolean(requestedMethod && /^(events|messageBrokers)\.(create|update)$/.test(requestedMethod));
|
|
4955
|
+
const enrichedData = isEventsSchema
|
|
4956
|
+
? {
|
|
4957
|
+
...compactData,
|
|
4958
|
+
method: requestedMethod,
|
|
4959
|
+
eventsCapability: {
|
|
4960
|
+
implementationWarning: EVENTS_IMPLEMENTATION_WARNING,
|
|
4961
|
+
deliverySemantics: EVENTS_DELIVERY_SEMANTICS,
|
|
4962
|
+
operations: eventsCapabilityOverview().operations,
|
|
4963
|
+
configurationValidation: {
|
|
4964
|
+
retryOrDlqUnspecified: 'Warn that provider defaults apply and inspect the deployed provider asset before claiming DLQ availability.',
|
|
4965
|
+
unsupportedFields: 'The live Joi schema rejects provider settings not supported by the selected env type; use the reported field path and provider matrix for remediation.',
|
|
4966
|
+
costSensitive: 'Azure Service Bus tier must be confirmed; provider operations may incur usage charges.',
|
|
4967
|
+
},
|
|
4968
|
+
documentationTopics: ['events', 'resilience'],
|
|
4969
|
+
},
|
|
4970
|
+
}
|
|
4971
|
+
: args.module === 'product' && !requestedMethod
|
|
4972
|
+
? {
|
|
4973
|
+
...compactData,
|
|
4974
|
+
eventsOwnership: {
|
|
4975
|
+
implementationWarning: EVENTS_IMPLEMENTATION_WARNING,
|
|
4976
|
+
deliverySemantics: EVENTS_DELIVERY_SEMANTICS,
|
|
4977
|
+
schemaMethods: ['events.create', 'events.update', 'messageBrokers.create', 'messageBrokers.update'],
|
|
4978
|
+
discoveryTool: 'ductape_events_discover',
|
|
4979
|
+
},
|
|
4980
|
+
}
|
|
4981
|
+
: compactData;
|
|
4982
|
+
return { content: [{ type: 'text', text: JSON.stringify(enrichedData ?? null, null, 2) }] };
|
|
4704
4983
|
}
|
|
4705
4984
|
catch (err) {
|
|
4706
4985
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4759,9 +5038,24 @@ async function main() {
|
|
|
4759
5038
|
'index strategy, operation types) that should be confirmed with the user first.\n\n' +
|
|
4760
5039
|
'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
4761
5040
|
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
4762
|
-
'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue',
|
|
5041
|
+
'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue',
|
|
4763
5042
|
inputSchema: docsInputSchema,
|
|
4764
5043
|
}, docsHandler);
|
|
5044
|
+
server.registerTool('ductape_events_discover', {
|
|
5045
|
+
title: 'Ductape Events Capability Discovery',
|
|
5046
|
+
description: 'Discover Ductape Events delivery ownership, retries, dead-letter queues, replay/reprocessing, observability operations, ' +
|
|
5047
|
+
'live input/output contracts, and provider-aware component configuration. Search recovery synonyms with query, or pass ' +
|
|
5048
|
+
'product and component together for live inspection. Ductape owns transport retries and exhausted-delivery DLQs; ' +
|
|
5049
|
+
'applications own transactional outboxes, domain rejection handling, and idempotent consumer mutations.',
|
|
5050
|
+
inputSchema: eventsDiscoveryInputSchema,
|
|
5051
|
+
}, eventsDiscoveryHandler);
|
|
5052
|
+
server.registerTool('ductape_function_setup', {
|
|
5053
|
+
title: 'Ductape Portable Function Setup',
|
|
5054
|
+
description: 'Generate the mandatory secure local + remote runtime setup for application functions used by Features. ' +
|
|
5055
|
+
'Use this whenever feature code calls application-owned logic. The result requires a registered local handler, ' +
|
|
5056
|
+
'a deterministic HTTPS endpoint, raw-body HMAC verification, runtime checks, and fail-closed behavior.',
|
|
5057
|
+
inputSchema: portableFunctionSetupInputSchema,
|
|
5058
|
+
}, portableFunctionSetupHandler);
|
|
4765
5059
|
server.registerTool('ductape_migration_plan', {
|
|
4766
5060
|
title: 'Ductape AI Migration Guidance',
|
|
4767
5061
|
description: 'Inspect a TypeScript, Go, Java, or .NET repository without exposing secret values. ' +
|
|
@@ -4886,6 +5180,8 @@ async function main() {
|
|
|
4886
5180
|
server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
|
|
4887
5181
|
server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
|
|
4888
5182
|
server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
|
|
5183
|
+
server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
|
|
5184
|
+
server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupHandler);
|
|
4889
5185
|
server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
|
|
4890
5186
|
server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
|
|
4891
5187
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;
|
|
1
|
+
{"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA4BD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAoGxF;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAiCZ"}
|
package/dist/proxy-client.js
CHANGED
|
@@ -28,28 +28,113 @@ export async function executeViaProxy(publishable_key, module, method, params =
|
|
|
28
28
|
}
|
|
29
29
|
return body.data?.data;
|
|
30
30
|
}
|
|
31
|
+
async function readJsonResponse(res, label) {
|
|
32
|
+
const contentType = res.headers.get('content-type')?.toLowerCase() ?? '';
|
|
33
|
+
if (!contentType.includes('json')) {
|
|
34
|
+
const responseText = await res.text();
|
|
35
|
+
const preview = responseText.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
36
|
+
throw new Error(`${label}: HTTP ${res.status} returned ${contentType || 'an unknown content type'}` +
|
|
37
|
+
(preview ? ` (${preview})` : ''));
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return await res.json();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
44
|
+
throw new Error(`${label}: HTTP ${res.status} returned invalid JSON (${detail})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
31
47
|
export async function getAssetSchemas(module, method) {
|
|
32
48
|
const path = module
|
|
33
49
|
? `/proxy/v1/schema/${encodeURIComponent(module)}${method ? `/${encodeURIComponent(method)}` : ''}`
|
|
34
50
|
: '/proxy/v1/schema';
|
|
35
51
|
const url = `${API_BASE_URL.replace(/\/$/, '')}${path}`;
|
|
36
52
|
const res = await fetch(url);
|
|
37
|
-
const body = (await res.json());
|
|
38
53
|
if (res.status === 404 && module && method) {
|
|
39
54
|
const fallbackUrl = `${API_BASE_URL.replace(/\/$/, '')}/proxy/v1/schema/${encodeURIComponent(module)}`;
|
|
40
55
|
const fallbackRes = await fetch(fallbackUrl);
|
|
41
|
-
const fallbackBody =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const fallbackBody = await readJsonResponse(fallbackRes, `Module schema fallback for ${module}.${method}`);
|
|
57
|
+
if (!fallbackRes.ok) {
|
|
58
|
+
throw new Error(fallbackBody.message ?? `Schema fallback request failed: ${fallbackRes.status}`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof fallbackBody.status === 'boolean' && !fallbackBody.status) {
|
|
61
|
+
throw new Error(fallbackBody.message ?? 'Schema fallback fetch failed');
|
|
45
62
|
}
|
|
63
|
+
const methods = fallbackBody.data?.method_schemas ?? fallbackBody.data?.methods;
|
|
64
|
+
const hasExactMethod = methods !== null && typeof methods === 'object' &&
|
|
65
|
+
!Array.isArray(methods) &&
|
|
66
|
+
Object.prototype.hasOwnProperty.call(methods, method);
|
|
67
|
+
const methodSchema = hasExactMethod ? methods[method] : undefined;
|
|
68
|
+
if (methodSchema !== null && typeof methodSchema === 'object') {
|
|
69
|
+
return {
|
|
70
|
+
module,
|
|
71
|
+
method,
|
|
72
|
+
schema: methodSchema,
|
|
73
|
+
resolution: {
|
|
74
|
+
exact: true,
|
|
75
|
+
source: fallbackBody.data?.method_schemas ? 'module.method_schemas' : 'module.methods',
|
|
76
|
+
targeted_route_status: res.status,
|
|
77
|
+
lookup_path: fallbackBody.data?.method_schemas
|
|
78
|
+
? `data.method_schemas[${JSON.stringify(method)}]`
|
|
79
|
+
: `data.methods[${JSON.stringify(method)}]`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(fallbackBody.data?.methods)) {
|
|
84
|
+
const rootUrl = `${API_BASE_URL.replace(/\/$/, '')}/proxy/v1/schema`;
|
|
85
|
+
const rootRes = await fetch(rootUrl);
|
|
86
|
+
const rootBody = await readJsonResponse(rootRes, `Root schema fallback for ${module}.${method}`);
|
|
87
|
+
if (!rootRes.ok) {
|
|
88
|
+
throw new Error(rootBody.message ?? `Root schema fallback request failed: ${rootRes.status}`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof rootBody.status === 'boolean' && !rootBody.status) {
|
|
91
|
+
throw new Error(rootBody.message ?? 'Root schema fallback fetch failed');
|
|
92
|
+
}
|
|
93
|
+
const moduleSchemas = rootBody.data?.modules?.[module];
|
|
94
|
+
const hasRootMethod = moduleSchemas !== null && typeof moduleSchemas === 'object' &&
|
|
95
|
+
!Array.isArray(moduleSchemas) &&
|
|
96
|
+
Object.prototype.hasOwnProperty.call(moduleSchemas, method);
|
|
97
|
+
const rootMethodSchema = hasRootMethod ? moduleSchemas[method] : undefined;
|
|
98
|
+
if (rootMethodSchema !== null && typeof rootMethodSchema === 'object') {
|
|
99
|
+
return {
|
|
100
|
+
module,
|
|
101
|
+
method,
|
|
102
|
+
schema: rootMethodSchema,
|
|
103
|
+
resolution: {
|
|
104
|
+
exact: true,
|
|
105
|
+
source: 'root.modules',
|
|
106
|
+
targeted_route_status: res.status,
|
|
107
|
+
lookup_path: `data.modules[${JSON.stringify(module)}][${JSON.stringify(method)}]`,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw new Error(hasExactMethod
|
|
113
|
+
? `Schema method "${method}" in the ${module} module schema is malformed`
|
|
114
|
+
: `Schema method "${method}" was not found in the ${module} module schema`);
|
|
46
115
|
}
|
|
116
|
+
const body = await readJsonResponse(res, 'Schema request');
|
|
47
117
|
if (!res.ok) {
|
|
48
118
|
throw new Error(body.message ?? `Schema request failed: ${res.status}`);
|
|
49
119
|
}
|
|
50
120
|
if (typeof body.status === 'boolean' && !body.status) {
|
|
51
121
|
throw new Error(body.message ?? 'Schema fetch failed');
|
|
52
122
|
}
|
|
123
|
+
if (module && method) {
|
|
124
|
+
if (body.data === null || typeof body.data !== 'object') {
|
|
125
|
+
throw new Error(`Targeted schema response for ${module}.${method} is malformed`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
module,
|
|
129
|
+
method,
|
|
130
|
+
schema: body.data,
|
|
131
|
+
resolution: {
|
|
132
|
+
exact: true,
|
|
133
|
+
source: 'targeted-route',
|
|
134
|
+
targeted_route_status: res.status,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
53
138
|
return body.data;
|
|
54
139
|
}
|
|
55
140
|
function normalizeTargets(targets) {
|
package/docs/TOOLS.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# MCP Tools Reference
|
|
2
2
|
|
|
3
|
-
The Ductape MCP server exposes
|
|
3
|
+
The Ductape MCP server exposes proxy, CLI, discovery, documentation, migration, and portable-function setup tools.
|
|
4
|
+
|
|
5
|
+
## Tool: `ductape_function_setup`
|
|
6
|
+
|
|
7
|
+
Produces the mandatory local registry and signed HTTPS exposure plan for portable application
|
|
8
|
+
functions referenced by Features. The route is
|
|
9
|
+
`/.well-known/ductape/functions/:namespace/:version/:operation` and requests use HMAC-SHA256.
|
|
4
10
|
|
|
5
11
|
---
|
|
6
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ductape/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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": "node scripts/check-frontend-analytics-guidance.mjs",
|
|
18
|
+
"test": "npm run build && 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",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|