@ductape/mcp 0.1.14 → 0.1.16

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.
Files changed (3) hide show
  1. package/dist/index.js +214 -60
  2. package/package.json +1 -1
  3. package/src/index.ts +214 -60
package/dist/index.js CHANGED
@@ -155,7 +155,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
155
155
  webhooks.events.list [app_tag, webhook_tag]
156
156
 
157
157
  ━━━ MODULE: sessions ━━━
158
- sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
158
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: Record<string, unknown> }]
159
+ ← selector MUST be in the format "$Session{fieldName}" (e.g. "$Session{playerId}"). Plain dot-paths are rejected.
160
+ ← schema is SAMPLE DATA — actual example values, not type declarations. The value at the selector path must be a primitive (string|number|boolean), not an object.
159
161
  sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
160
162
  sessions.fetch [product_tag, session_tag]
161
163
  sessions.list [product_tag]
@@ -291,8 +293,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
291
293
  messageBrokers.fetch [product_tag, broker_tag]
292
294
  messageBrokers.list [product_tag]
293
295
  messageBrokers.delete [product_tag, broker_tag]
294
- messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
295
- messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
296
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
297
+ description?: string, sample?: object, idempotent?: boolean,
298
+ queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
299
+ }]
300
+ ← A broker can have unlimited topics. Add one per logical event type.
301
+ ← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
302
+ ← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
303
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
304
+ sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
296
305
  messageBrokers.topics.fetch [product_tag, topic_tag]
297
306
  messageBrokers.topics.list [product_tag, broker_tag]
298
307
  messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
@@ -578,13 +587,14 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
578
587
  features.compare [executionId1: string, executionId2: string]
579
588
 
580
589
  ━━━ MODULE: caches ━━━
581
- caches.create [{ product, tag, name, description?, type: "redis"|"memcached"|"in-memory", envs: [{slug, connection_url}] }]
590
+ caches.create [product_tag, data: { name: string, tag: string, description?: string, expiry: number }]
591
+ ← expiry is in SECONDS (e.g. 3600 = 1 hour, 86400 = 1 day). No type or envs — Ductape manages the store.
582
592
  caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
583
593
  caches.fetch [product_tag, cache_tag]
584
594
  caches.list [product_tag]
585
595
  caches.delete [product_tag, cache_tag]
586
596
  caches.get [{ key: string }]
587
- caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
597
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
588
598
  caches.clear [{ key: string }]
589
599
  caches.clearAll [{ product, cache, env? }]
590
600
  caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
@@ -1618,24 +1628,53 @@ Bootstrap (single API call returning product context + component config + privat
1618
1628
  sessions: `
1619
1629
  DUCTAPE SESSIONS
1620
1630
 
1621
- A session is a named JWT schema on a product. It defines: a tag, an expiry duration, a selector
1622
- (which field in the data object is the user identifier), and a schema (the shape of the JWT payload).
1623
-
1624
- Define a session (admin ductape_execute):
1631
+ A session is a named JWT schema on a product. It defines:
1632
+ - tag / name — unique identifier and display name
1633
+ - expiry + period — how long each issued JWT is valid (duration, not an absolute date)
1634
+ - selector — MUST be in the format "$Session{fieldName}" where fieldName is the key
1635
+ in the schema that is the PRIMARY user identifier (e.g. "$Session{playerId}").
1636
+ Plain dot-paths like "playerId" are REJECTED by the validator.
1637
+ This field becomes the lookup key for revoke, list, and analytics.
1638
+ - schema — SAMPLE DATA showing example values for each field embedded in the JWT.
1639
+ This is NOT a type declaration. Use actual example values.
1640
+ The value at the selector path must be a primitive (string/number/boolean),
1641
+ not an object or array.
1642
+
1643
+ IMPORTANT — schema is sample data, not type declarations:
1644
+ CORRECT: schema: { playerId: "player_abc123", username: "Alice", role: "player" }
1645
+ INCORRECT: schema: { playerId: { type: "string", required: true } } ← WILL FAIL
1646
+
1647
+ IMPORTANT — selector must be "$Session{fieldName}" format:
1648
+ CORRECT: selector: "$Session{playerId}"
1649
+ INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
1650
+
1651
+ Example (game product — player identity in JWT):
1625
1652
  ductape_execute("sessions.create", [product_tag, {
1626
- tag: "user-session",
1627
- name: "User Session",
1653
+ tag: "player-session",
1654
+ name: "Player Session",
1628
1655
  expiry: 24,
1629
- period: "hours", // "seconds" | "minutes" | "hours" | "days"
1630
- selector: "userId", // field in data that identifies the user
1631
- schema: { userId: "string", role: "string", email: "string" },
1656
+ period: "hours",
1657
+ selector: "$Session{playerId}", // $Session{} wrapper required
1658
+ schema: {
1659
+ playerId: "player_abc123", // sample value — primitive required at selector path
1660
+ username: "ShadowBlade",
1661
+ role: "player",
1662
+ accountId: "acct_xyz",
1663
+ },
1632
1664
  }])
1633
1665
 
1634
1666
  Runtime — create a session (sign a JWT):
1635
1667
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
1636
- ductape_execute("sessions.start", [{ product, env, tag: "user-session",
1637
- data: { userId: "u_123", role: "admin", email: "user@example.com" } }])
1638
- → returns token: "user-session:eyJ..." ← format is always "session_tag:jwt"
1668
+ to discover the exact data field names accepted for this session tag.
1669
+ ductape_execute("sessions.start", [{ product, env, tag: "player-session",
1670
+ data: {
1671
+ playerId: "player_abc123", // must match selector path — used as the revocation/lookup key
1672
+ username: "ShadowBlade",
1673
+ role: "player",
1674
+ accountId: "acct_xyz",
1675
+ } }])
1676
+ → returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
1677
+ The token embeds all schema fields in the JWT payload, signed with the product private key.
1639
1678
 
1640
1679
  Verify a token:
1641
1680
  ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
@@ -1666,17 +1705,25 @@ DUCTAPE CACHES
1666
1705
 
1667
1706
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
1668
1707
 
1669
- Registration (admin — ductape_cli):
1708
+ Registration (admin — ductape_cli or SDK):
1670
1709
  ductape_cli("resources caches create -f cache.json")
1671
- File: { name, tag, type: "redis"|"memcached"|"in-memory",
1672
- envs: [{ slug, connection_url }] }
1710
+ File: { name, tag, description?, expiry: <seconds> }
1711
+ No type or envs Ductape manages the store infrastructure.
1712
+ expiry is in SECONDS: 3600 = 1 hour, 86400 = 1 day, 604800 = 1 week.
1713
+
1714
+ SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600 }])
1673
1715
 
1674
1716
  Operations:
1675
- caches.set [{ product, cache, key, value: string, expiry?: Date, env }]
1717
+ caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
1718
+ expiry is an ABSOLUTE TIMESTAMP (not a duration).
1719
+ To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
1720
+ To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
1721
+ To never expire: omit expiry entirely.
1722
+ Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
1676
1723
  → Writes to Redis synchronously, then fires remote API write in background (non-blocking).
1677
1724
  caches.get [{ key: string }]
1678
1725
  → Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
1679
- → Enforces TTL by comparing stored expiry field against current time.
1726
+ → Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
1680
1727
  caches.clear [{ key: string }]
1681
1728
  → Deletes from Redis and from remote API.
1682
1729
  caches.clearAll [{ product, cache, env? }]
@@ -1691,13 +1738,22 @@ Tier architecture (three tiers applied automatically):
1691
1738
  Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
1692
1739
  Tier 3: Remote Ductape API
1693
1740
 
1741
+ Practical examples:
1742
+ // Cache a player leaderboard for 5 minutes:
1743
+ { product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
1744
+ expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
1745
+
1746
+ // Cache a session token lookup for 1 hour:
1747
+ { product, cache: "session-cache", key: "player:u_123", value: token,
1748
+ expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
1749
+
1694
1750
  Important:
1695
1751
  - Redis is optional; without it all reads/writes go through the remote API.
1696
- - expiry is a stored Date field, not a Redis TTL — expiry check happens client-side.
1697
- - clearAll only clears via remote API; Redis retains stale entries until accessed and found expired.
1752
+ - expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
1753
+ - clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
1698
1754
  - Cache entries are stored as Redis hashes (not plain strings).
1699
- - Other services (storage, graph, notifications, sessions) also use the CacheManager for
1700
- their own result caching — configure a shared Redis URL at SDK init to share the pool.
1755
+ - Other services (storage, graph, notifications, sessions) also use CacheManager internally —
1756
+ configure a shared Redis URL at SDK init to share the pool.
1701
1757
  `.trim(),
1702
1758
  notifications: `
1703
1759
  DUCTAPE NOTIFICATIONS
@@ -1885,55 +1941,153 @@ Code-first (define API — compiles async handler to JSON step schema):
1885
1941
  events: `
1886
1942
  DUCTAPE EVENTS (MESSAGE BROKERS)
1887
1943
 
1888
- Supported broker types: kafka | rabbitmq | redis | aws_sqs | azure_servicebus | google_pubsub | nats
1944
+ ARCHITECTURE always two separate steps:
1945
+ Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
1946
+ The broker's envs[] holds connection credentials and host/project info, NOT topics.
1947
+ Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
1948
+ Topics are separate from the broker registration and added after.
1949
+ A single broker component can have as many topics as needed.
1950
+
1951
+ ━━━ SUPPORTED BROKER TYPES ━━━
1952
+
1953
+ Cloud-managed (provision OR import via cloud connection):
1954
+ GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
1955
+ AWS SQS → service: "sqs", type in envs: "aws_sqs"
1956
+ Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
1957
+
1958
+ Self-hosted (import-only — supply connection URL manually):
1959
+ Kafka → type in envs: "kafka"
1960
+ RabbitMQ → type in envs: "rabbitmq"
1961
+ Redis → type in envs: "redis"
1962
+ NATS → type in envs: "nats"
1963
+
1964
+ ━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
1965
+
1966
+ Provision (create a NEW resource in the cloud):
1967
+ ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
1968
+ File: JSON ARRAY — one entry per env, same product + component tag across all entries.
1969
+ type field: "messageBrokers" (exact — not "messagebrokers" or "events")
1970
+
1971
+ GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
1972
+ Cost: GCP Pub/Sub is usage-based — no upfront cost, no tier selection required.
1973
+ You pay per GB of data published/subscribed (first 10 GB/month free).
1974
+ It is safe to provision without user approval of a fixed monthly cost.
1975
+ [{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
1976
+ "product":"my-product","component":"notifications-broker","env":"snd",
1977
+ "topicName":"my-product-notifications-snd"},
1978
+ {"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
1979
+ "product":"my-product","component":"notifications-broker","env":"prd",
1980
+ "topicName":"my-product-notifications-prd"}]
1981
+ If topicName is omitted a timestamped name is generated — always supply it explicitly.
1982
+
1983
+ AWS SQS — creates a new SQS queue per env:
1984
+ Cost: SQS is usage-based — no upfront cost, no tier selection required.
1985
+ First 1 million requests/month free; $0.40 per million after that.
1986
+ It is safe to provision without user approval of a fixed monthly cost.
1987
+ [{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
1988
+ "product":"my-product","component":"notifications-broker","env":"snd",
1989
+ "queueName":"my-product-notifications-snd"},
1990
+ {"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
1991
+ "product":"my-product","component":"notifications-broker","env":"prd",
1992
+ "queueName":"my-product-notifications-prd"}]
1993
+
1994
+ Azure Service Bus — creates a namespace + queue per env:
1995
+ Cost: Azure Service Bus has TIERED pricing — confirm the tier with the user before provisioning.
1996
+ Basic: queues only, ~$0.05/million operations. No topics/subscriptions.
1997
+ Standard: queues + topics, ~$10/month base + $0.10/million operations.
1998
+ Premium: dedicated capacity, starts ~$677/month. Not needed for standard workloads.
1999
+ DO NOT provision Azure Service Bus without confirming the tier with the user.
2000
+ [{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
2001
+ "product":"my-product","component":"notifications-broker","env":"snd",
2002
+ "namespaceName":"myproduct-snd","queueName":"notifications"},
2003
+ {"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
2004
+ "product":"my-product","component":"notifications-broker","env":"prd",
2005
+ "namespaceName":"myproduct-prd","queueName":"notifications"}]
2006
+
2007
+ Import (register an EXISTING cloud resource):
2008
+ Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
2009
+ ductape_cli("cloud resources import-persist-all -f brokers.json --json")
2010
+ Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
1889
2011
 
1890
- CLI aliases for the messageBrokers module: events, event, broker, brokers, message-brokers
1891
- ductape_cli("resources events list <product_tag> --json")
2012
+ IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
2013
+ separate resources per env to avoid mixing sandbox and production events.
1892
2014
 
1893
- Registration (admin ductape_cli):
1894
- Message brokers are IMPORT-ONLY (no provision-persist). Use import-persist-all:
1895
- ductape_cli("cloud resources import-persist-all -f brokers.json --json")
1896
- File is a JSON ARRAY — one entry per env.
1897
- service identifiers: pubsub (GCP Pub/Sub) | sqs (AWS SQS) | servicebus (Azure Service Bus)
1898
- type field: "messageBrokers" (not "messagebrokers" or "events")
2015
+ ━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
2016
+
2017
+ ductape_cli("resources events create -f broker.json")
2018
+ File: {
2019
+ name: string, tag: string, description?: string,
2020
+ envs: [
2021
+ {
2022
+ slug: "snd",
2023
+ type: "kafka"|"rabbitmq"|"redis"|"nats",
2024
+ config: <see config shapes below>
2025
+ },
2026
+ { slug: "prd", type: "kafka", config: { ... } }
2027
+ ]
2028
+ }
2029
+
2030
+ Config shapes per type:
2031
+ kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
2032
+ ssl?: true, sasl?: { mechanism: "plain", username, password } }
2033
+ rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
2034
+ redis: { host: "...", port: 6379, password?: "..." }
2035
+ nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
2036
+
2037
+ ━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
1899
2038
 
1900
- After importing, create topics:
1901
2039
  ductape_execute("messageBrokers.topics.create", [product_tag, {
1902
- tag: "user-events", name: "User Events", broker: "broker-tag",
1903
- type: "producer"|"consumer"|"both",
2040
+ tag: "player-joined",
2041
+ name: "Player Joined",
2042
+ broker: "notifications-broker", // broker component tag
2043
+ description?: string,
2044
+ sample: { playerId: "string", username: "string" }, // example message shape
2045
+ idempotent?: boolean,
2046
+ // SQS only — map per-env queue URLs (each topic can be a different queue):
2047
+ queueUrls?: [
2048
+ { env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
2049
+ { env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
2050
+ ]
1904
2051
  }])
1905
2052
 
1906
- Produce a message (runtime):
1907
- CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
1908
- messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
1909
- Idempotent publish:
1910
- messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
1911
- → checks if key was already processed; returns cached result if so.
2053
+ For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
2054
+ config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
2055
+ For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
2056
+ Repeat this call for each logical event type — there is no limit on number of topics.
1912
2057
 
1913
- Consume a message (subscribe):
1914
- messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
1915
- callback: async (message) => { ... } }]
1916
- Callback tracking is deferred (setImmediate) so user callback latency is unaffected.
1917
- Callback errors are re-thrown so the broker can nack/retry.
2058
+ List topics on a broker:
2059
+ ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
1918
2060
 
1919
- Background dispatch with scheduling:
1920
- messageBrokers.dispatch [{ product, env, broker, event, input: { message },
1921
- schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1922
- → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2061
+ ━━━ RUNTIME: PRODUCE AND CONSUME ━━━
2062
+
2063
+ Produce (publish a message):
2064
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
2065
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
2066
+ Idempotent publish (deduplicates by key, default TTL 86400 s):
2067
+ messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
2068
+
2069
+ Consume (subscribe):
2070
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
2071
+ callback: async (message) => { ... } }]
2072
+ Callback errors are re-thrown so the broker can nack/retry.
2073
+
2074
+ Background dispatch with scheduling:
2075
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message },
2076
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
2077
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2078
+
2079
+ Event format string: "broker_tag:topic_tag" — always colon-separated.
2080
+ Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
2081
+
2082
+ ━━━ OBSERVABILITY ━━━
1923
2083
 
1924
- Event tracking and observability:
1925
2084
  messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
1926
2085
  messageBrokers.messages.getStats [{ product, env, brokerTag }]
1927
- → { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
1928
2086
  messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
1929
2087
  messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
1930
2088
  messageBrokers.replayEvent [{ product, env, eventId, force? }]
1931
2089
  messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
1932
- messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }] → { exists, event_id? }
1933
-
1934
- Event format string: "broker_tag:topic_tag" — always colon-separated; parsed by the SDK.
1935
- Message payload is AES-encrypted before the tracking API call — tracking endpoint never sees plaintext.
1936
- Connection pool: deduplicates live connections across BrokersService instances by workspace+product+config.
2090
+ messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
1937
2091
  `.trim(),
1938
2092
  logs: `
1939
2093
  DUCTAPE LOGS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -166,7 +166,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
166
166
  webhooks.events.list [app_tag, webhook_tag]
167
167
 
168
168
  ━━━ MODULE: sessions ━━━
169
- sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
169
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: Record<string, unknown> }]
170
+ ← selector MUST be in the format "$Session{fieldName}" (e.g. "$Session{playerId}"). Plain dot-paths are rejected.
171
+ ← schema is SAMPLE DATA — actual example values, not type declarations. The value at the selector path must be a primitive (string|number|boolean), not an object.
170
172
  sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
171
173
  sessions.fetch [product_tag, session_tag]
172
174
  sessions.list [product_tag]
@@ -302,8 +304,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
302
304
  messageBrokers.fetch [product_tag, broker_tag]
303
305
  messageBrokers.list [product_tag]
304
306
  messageBrokers.delete [product_tag, broker_tag]
305
- messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
306
- messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
307
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
308
+ description?: string, sample?: object, idempotent?: boolean,
309
+ queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
310
+ }]
311
+ ← A broker can have unlimited topics. Add one per logical event type.
312
+ ← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
313
+ ← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
314
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
315
+ sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
307
316
  messageBrokers.topics.fetch [product_tag, topic_tag]
308
317
  messageBrokers.topics.list [product_tag, broker_tag]
309
318
  messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
@@ -589,13 +598,14 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
589
598
  features.compare [executionId1: string, executionId2: string]
590
599
 
591
600
  ━━━ MODULE: caches ━━━
592
- caches.create [{ product, tag, name, description?, type: "redis"|"memcached"|"in-memory", envs: [{slug, connection_url}] }]
601
+ caches.create [product_tag, data: { name: string, tag: string, description?: string, expiry: number }]
602
+ ← expiry is in SECONDS (e.g. 3600 = 1 hour, 86400 = 1 day). No type or envs — Ductape manages the store.
593
603
  caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
594
604
  caches.fetch [product_tag, cache_tag]
595
605
  caches.list [product_tag]
596
606
  caches.delete [product_tag, cache_tag]
597
607
  caches.get [{ key: string }]
598
- caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
608
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
599
609
  caches.clear [{ key: string }]
600
610
  caches.clearAll [{ product, cache, env? }]
601
611
  caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
@@ -1682,24 +1692,53 @@ Bootstrap (single API call returning product context + component config + privat
1682
1692
  sessions: `
1683
1693
  DUCTAPE SESSIONS
1684
1694
 
1685
- A session is a named JWT schema on a product. It defines: a tag, an expiry duration, a selector
1686
- (which field in the data object is the user identifier), and a schema (the shape of the JWT payload).
1687
-
1688
- Define a session (admin ductape_execute):
1695
+ A session is a named JWT schema on a product. It defines:
1696
+ - tag / name — unique identifier and display name
1697
+ - expiry + period — how long each issued JWT is valid (duration, not an absolute date)
1698
+ - selector — MUST be in the format "$Session{fieldName}" where fieldName is the key
1699
+ in the schema that is the PRIMARY user identifier (e.g. "$Session{playerId}").
1700
+ Plain dot-paths like "playerId" are REJECTED by the validator.
1701
+ This field becomes the lookup key for revoke, list, and analytics.
1702
+ - schema — SAMPLE DATA showing example values for each field embedded in the JWT.
1703
+ This is NOT a type declaration. Use actual example values.
1704
+ The value at the selector path must be a primitive (string/number/boolean),
1705
+ not an object or array.
1706
+
1707
+ IMPORTANT — schema is sample data, not type declarations:
1708
+ CORRECT: schema: { playerId: "player_abc123", username: "Alice", role: "player" }
1709
+ INCORRECT: schema: { playerId: { type: "string", required: true } } ← WILL FAIL
1710
+
1711
+ IMPORTANT — selector must be "$Session{fieldName}" format:
1712
+ CORRECT: selector: "$Session{playerId}"
1713
+ INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
1714
+
1715
+ Example (game product — player identity in JWT):
1689
1716
  ductape_execute("sessions.create", [product_tag, {
1690
- tag: "user-session",
1691
- name: "User Session",
1717
+ tag: "player-session",
1718
+ name: "Player Session",
1692
1719
  expiry: 24,
1693
- period: "hours", // "seconds" | "minutes" | "hours" | "days"
1694
- selector: "userId", // field in data that identifies the user
1695
- schema: { userId: "string", role: "string", email: "string" },
1720
+ period: "hours",
1721
+ selector: "$Session{playerId}", // $Session{} wrapper required
1722
+ schema: {
1723
+ playerId: "player_abc123", // sample value — primitive required at selector path
1724
+ username: "ShadowBlade",
1725
+ role: "player",
1726
+ accountId: "acct_xyz",
1727
+ },
1696
1728
  }])
1697
1729
 
1698
1730
  Runtime — create a session (sign a JWT):
1699
1731
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
1700
- ductape_execute("sessions.start", [{ product, env, tag: "user-session",
1701
- data: { userId: "u_123", role: "admin", email: "user@example.com" } }])
1702
- → returns token: "user-session:eyJ..." ← format is always "session_tag:jwt"
1732
+ to discover the exact data field names accepted for this session tag.
1733
+ ductape_execute("sessions.start", [{ product, env, tag: "player-session",
1734
+ data: {
1735
+ playerId: "player_abc123", // must match selector path — used as the revocation/lookup key
1736
+ username: "ShadowBlade",
1737
+ role: "player",
1738
+ accountId: "acct_xyz",
1739
+ } }])
1740
+ → returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
1741
+ The token embeds all schema fields in the JWT payload, signed with the product private key.
1703
1742
 
1704
1743
  Verify a token:
1705
1744
  ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
@@ -1731,17 +1770,25 @@ DUCTAPE CACHES
1731
1770
 
1732
1771
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
1733
1772
 
1734
- Registration (admin — ductape_cli):
1773
+ Registration (admin — ductape_cli or SDK):
1735
1774
  ductape_cli("resources caches create -f cache.json")
1736
- File: { name, tag, type: "redis"|"memcached"|"in-memory",
1737
- envs: [{ slug, connection_url }] }
1775
+ File: { name, tag, description?, expiry: <seconds> }
1776
+ No type or envs Ductape manages the store infrastructure.
1777
+ expiry is in SECONDS: 3600 = 1 hour, 86400 = 1 day, 604800 = 1 week.
1778
+
1779
+ SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600 }])
1738
1780
 
1739
1781
  Operations:
1740
- caches.set [{ product, cache, key, value: string, expiry?: Date, env }]
1782
+ caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
1783
+ expiry is an ABSOLUTE TIMESTAMP (not a duration).
1784
+ To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
1785
+ To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
1786
+ To never expire: omit expiry entirely.
1787
+ Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
1741
1788
  → Writes to Redis synchronously, then fires remote API write in background (non-blocking).
1742
1789
  caches.get [{ key: string }]
1743
1790
  → Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
1744
- → Enforces TTL by comparing stored expiry field against current time.
1791
+ → Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
1745
1792
  caches.clear [{ key: string }]
1746
1793
  → Deletes from Redis and from remote API.
1747
1794
  caches.clearAll [{ product, cache, env? }]
@@ -1756,13 +1803,22 @@ Tier architecture (three tiers applied automatically):
1756
1803
  Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
1757
1804
  Tier 3: Remote Ductape API
1758
1805
 
1806
+ Practical examples:
1807
+ // Cache a player leaderboard for 5 minutes:
1808
+ { product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
1809
+ expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
1810
+
1811
+ // Cache a session token lookup for 1 hour:
1812
+ { product, cache: "session-cache", key: "player:u_123", value: token,
1813
+ expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
1814
+
1759
1815
  Important:
1760
1816
  - Redis is optional; without it all reads/writes go through the remote API.
1761
- - expiry is a stored Date field, not a Redis TTL — expiry check happens client-side.
1762
- - clearAll only clears via remote API; Redis retains stale entries until accessed and found expired.
1817
+ - expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
1818
+ - clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
1763
1819
  - Cache entries are stored as Redis hashes (not plain strings).
1764
- - Other services (storage, graph, notifications, sessions) also use the CacheManager for
1765
- their own result caching — configure a shared Redis URL at SDK init to share the pool.
1820
+ - Other services (storage, graph, notifications, sessions) also use CacheManager internally —
1821
+ configure a shared Redis URL at SDK init to share the pool.
1766
1822
  `.trim(),
1767
1823
 
1768
1824
  notifications: `
@@ -1954,55 +2010,153 @@ Code-first (define API — compiles async handler to JSON step schema):
1954
2010
  events: `
1955
2011
  DUCTAPE EVENTS (MESSAGE BROKERS)
1956
2012
 
1957
- Supported broker types: kafka | rabbitmq | redis | aws_sqs | azure_servicebus | google_pubsub | nats
2013
+ ARCHITECTURE always two separate steps:
2014
+ Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
2015
+ The broker's envs[] holds connection credentials and host/project info, NOT topics.
2016
+ Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
2017
+ Topics are separate from the broker registration and added after.
2018
+ A single broker component can have as many topics as needed.
2019
+
2020
+ ━━━ SUPPORTED BROKER TYPES ━━━
2021
+
2022
+ Cloud-managed (provision OR import via cloud connection):
2023
+ GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
2024
+ AWS SQS → service: "sqs", type in envs: "aws_sqs"
2025
+ Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
2026
+
2027
+ Self-hosted (import-only — supply connection URL manually):
2028
+ Kafka → type in envs: "kafka"
2029
+ RabbitMQ → type in envs: "rabbitmq"
2030
+ Redis → type in envs: "redis"
2031
+ NATS → type in envs: "nats"
2032
+
2033
+ ━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
2034
+
2035
+ Provision (create a NEW resource in the cloud):
2036
+ ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
2037
+ File: JSON ARRAY — one entry per env, same product + component tag across all entries.
2038
+ type field: "messageBrokers" (exact — not "messagebrokers" or "events")
2039
+
2040
+ GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
2041
+ Cost: GCP Pub/Sub is usage-based — no upfront cost, no tier selection required.
2042
+ You pay per GB of data published/subscribed (first 10 GB/month free).
2043
+ It is safe to provision without user approval of a fixed monthly cost.
2044
+ [{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
2045
+ "product":"my-product","component":"notifications-broker","env":"snd",
2046
+ "topicName":"my-product-notifications-snd"},
2047
+ {"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
2048
+ "product":"my-product","component":"notifications-broker","env":"prd",
2049
+ "topicName":"my-product-notifications-prd"}]
2050
+ If topicName is omitted a timestamped name is generated — always supply it explicitly.
2051
+
2052
+ AWS SQS — creates a new SQS queue per env:
2053
+ Cost: SQS is usage-based — no upfront cost, no tier selection required.
2054
+ First 1 million requests/month free; $0.40 per million after that.
2055
+ It is safe to provision without user approval of a fixed monthly cost.
2056
+ [{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
2057
+ "product":"my-product","component":"notifications-broker","env":"snd",
2058
+ "queueName":"my-product-notifications-snd"},
2059
+ {"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
2060
+ "product":"my-product","component":"notifications-broker","env":"prd",
2061
+ "queueName":"my-product-notifications-prd"}]
2062
+
2063
+ Azure Service Bus — creates a namespace + queue per env:
2064
+ Cost: Azure Service Bus has TIERED pricing — confirm the tier with the user before provisioning.
2065
+ Basic: queues only, ~$0.05/million operations. No topics/subscriptions.
2066
+ Standard: queues + topics, ~$10/month base + $0.10/million operations.
2067
+ Premium: dedicated capacity, starts ~$677/month. Not needed for standard workloads.
2068
+ DO NOT provision Azure Service Bus without confirming the tier with the user.
2069
+ [{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
2070
+ "product":"my-product","component":"notifications-broker","env":"snd",
2071
+ "namespaceName":"myproduct-snd","queueName":"notifications"},
2072
+ {"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
2073
+ "product":"my-product","component":"notifications-broker","env":"prd",
2074
+ "namespaceName":"myproduct-prd","queueName":"notifications"}]
2075
+
2076
+ Import (register an EXISTING cloud resource):
2077
+ Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
2078
+ ductape_cli("cloud resources import-persist-all -f brokers.json --json")
2079
+ Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
1958
2080
 
1959
- CLI aliases for the messageBrokers module: events, event, broker, brokers, message-brokers
1960
- ductape_cli("resources events list <product_tag> --json")
2081
+ IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
2082
+ separate resources per env to avoid mixing sandbox and production events.
1961
2083
 
1962
- Registration (admin ductape_cli):
1963
- Message brokers are IMPORT-ONLY (no provision-persist). Use import-persist-all:
1964
- ductape_cli("cloud resources import-persist-all -f brokers.json --json")
1965
- File is a JSON ARRAY — one entry per env.
1966
- service identifiers: pubsub (GCP Pub/Sub) | sqs (AWS SQS) | servicebus (Azure Service Bus)
1967
- type field: "messageBrokers" (not "messagebrokers" or "events")
2084
+ ━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
2085
+
2086
+ ductape_cli("resources events create -f broker.json")
2087
+ File: {
2088
+ name: string, tag: string, description?: string,
2089
+ envs: [
2090
+ {
2091
+ slug: "snd",
2092
+ type: "kafka"|"rabbitmq"|"redis"|"nats",
2093
+ config: <see config shapes below>
2094
+ },
2095
+ { slug: "prd", type: "kafka", config: { ... } }
2096
+ ]
2097
+ }
2098
+
2099
+ Config shapes per type:
2100
+ kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
2101
+ ssl?: true, sasl?: { mechanism: "plain", username, password } }
2102
+ rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
2103
+ redis: { host: "...", port: 6379, password?: "..." }
2104
+ nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
2105
+
2106
+ ━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
1968
2107
 
1969
- After importing, create topics:
1970
2108
  ductape_execute("messageBrokers.topics.create", [product_tag, {
1971
- tag: "user-events", name: "User Events", broker: "broker-tag",
1972
- type: "producer"|"consumer"|"both",
2109
+ tag: "player-joined",
2110
+ name: "Player Joined",
2111
+ broker: "notifications-broker", // broker component tag
2112
+ description?: string,
2113
+ sample: { playerId: "string", username: "string" }, // example message shape
2114
+ idempotent?: boolean,
2115
+ // SQS only — map per-env queue URLs (each topic can be a different queue):
2116
+ queueUrls?: [
2117
+ { env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
2118
+ { env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
2119
+ ]
1973
2120
  }])
1974
2121
 
1975
- Produce a message (runtime):
1976
- CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
1977
- messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
1978
- Idempotent publish:
1979
- messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
1980
- → checks if key was already processed; returns cached result if so.
2122
+ For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
2123
+ config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
2124
+ For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
2125
+ Repeat this call for each logical event type — there is no limit on number of topics.
1981
2126
 
1982
- Consume a message (subscribe):
1983
- messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
1984
- callback: async (message) => { ... } }]
1985
- Callback tracking is deferred (setImmediate) so user callback latency is unaffected.
1986
- Callback errors are re-thrown so the broker can nack/retry.
2127
+ List topics on a broker:
2128
+ ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
1987
2129
 
1988
- Background dispatch with scheduling:
1989
- messageBrokers.dispatch [{ product, env, broker, event, input: { message },
1990
- schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1991
- → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2130
+ ━━━ RUNTIME: PRODUCE AND CONSUME ━━━
2131
+
2132
+ Produce (publish a message):
2133
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
2134
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
2135
+ Idempotent publish (deduplicates by key, default TTL 86400 s):
2136
+ messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
2137
+
2138
+ Consume (subscribe):
2139
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
2140
+ callback: async (message) => { ... } }]
2141
+ Callback errors are re-thrown so the broker can nack/retry.
2142
+
2143
+ Background dispatch with scheduling:
2144
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message },
2145
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
2146
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2147
+
2148
+ Event format string: "broker_tag:topic_tag" — always colon-separated.
2149
+ Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
2150
+
2151
+ ━━━ OBSERVABILITY ━━━
1992
2152
 
1993
- Event tracking and observability:
1994
2153
  messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
1995
2154
  messageBrokers.messages.getStats [{ product, env, brokerTag }]
1996
- → { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
1997
2155
  messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
1998
2156
  messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
1999
2157
  messageBrokers.replayEvent [{ product, env, eventId, force? }]
2000
2158
  messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
2001
- messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }] → { exists, event_id? }
2002
-
2003
- Event format string: "broker_tag:topic_tag" — always colon-separated; parsed by the SDK.
2004
- Message payload is AES-encrypted before the tracking API call — tracking endpoint never sees plaintext.
2005
- Connection pool: deduplicates live connections across BrokersService instances by workspace+product+config.
2159
+ messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
2006
2160
  `.trim(),
2007
2161
 
2008
2162
  logs: `