@ductape/mcp 0.2.14 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Added source-verified graph/vector projection guidance to the `features`, `events`, `graphs`, and `vector` docs topics, including exact graph/vector payloads, NestJS consumer routing, replay/idempotency, projection-state ordering, rebuild/drift repair, and an explicit stop condition for the currently missing code-first `ctx.vector` API.
9
10
  - Added read-only `ductape_events_topic_setup` and `ductape_events_validate_project` tools and made the canonical one-topic-per-file `ductape/events/<topic-tag>.topic.json` layout explicit and enforceable through the CLI.
10
11
  - Mark `ductape_function_setup` as read-only, non-destructive, idempotent, and closed-world in both MCP registration APIs so hosts do not incorrectly require mutation approval for its pure setup-plan generation.
11
12
  - Broadened Feature guidance from durable/event-driven workflows to synchronous or asynchronous named product capabilities.
package/dist/index.js CHANGED
@@ -805,7 +805,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
805
805
  graph.dropConstraint [name: string]
806
806
  graph.listLabels []
807
807
  graph.listRelationshipTypes []
808
- graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
808
+ graph.createAction [{ graphTag, name, description?, operation, query: { operation, options }, parameters: [{ name, path, defaultValue, type, description?, required? }] }, productTag?]
809
809
  graph.listActions [graphTag?, productTag?]
810
810
  graph.getAction [actionTag, graphTag?, productTag?]
811
811
  graph.updateAction [actionTag, updates, graphTag?, productTag?]
@@ -2396,8 +2396,9 @@ Schema management:
2396
2396
  graph.dropIndex / dropConstraint / listIndexes / listConstraints
2397
2397
 
2398
2398
  Saved actions (parameterized queries stored on the product):
2399
- graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
2399
+ graph.createAction [{ graphTag?, name, description?, operation, query, parameters: [{ name, type, required?, defaultValue?, description? }] }, productTag?]
2400
2400
  graph.listActions [graphTag?, productTag?]
2401
+ graph.execute [{ product, env, graph, action, input?, session?, cache? }]
2401
2402
  graph.dispatch [data] ← call ductape_generate_payload FIRST
2402
2403
 
2403
2404
  Supported index types: btree | fulltext | vector | range | point | text
@@ -2512,7 +2513,7 @@ Registration (admin — ductape_cli):
2512
2513
  Runtime operations:
2513
2514
  vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
2514
2515
  vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace? }]
2515
- vector.query [{ product, env, tag, vector: number[], topK?, filter?, namespace?,
2516
+ vector.query [{ product, env, tag, vector: number[], topK: number, filter?, namespace?,
2516
2517
  includeValues?, includeMetadata? }]
2517
2518
  vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace? }]
2518
2519
  vector.fetchOne [{ product, env, vector, id, namespace? }]
@@ -3615,14 +3616,15 @@ STEP 8 — SET rollbacks for reversible steps
3615
3616
  async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
3616
3617
  );
3617
3618
 
3618
- Step types: function | action | database | graph | notification | storage | produce | quota |
3619
- fallback | vector | child_feature | sleep | wait_for_signal | checkpoint
3619
+ Code-first ctx step types currently record: function | action | database | graph | vector |
3620
+ notification | storage | produce | quota | fallback | child_feature | sleep | wait_for_signal |
3621
+ checkpoint.
3620
3622
 
3621
3623
  Valid synchronous Feature candidates include generate-world, resolve-nation-turn,
3622
3624
  calculate-route-capacity, price-subscription, evaluate-entitlement, and build-replay.
3623
3625
 
3624
3626
  Synchronous multi-step Feature (no Event, schedule, sleep, signal, or external system):
3625
- await ductape.features.define({
3627
+ await ductape.feature.define({
3626
3628
  product: 'example-product',
3627
3629
  tag: 'resolve-nation-turn',
3628
3630
  name: 'Resolve Nation Turn',
@@ -3635,15 +3637,10 @@ Synchronous multi-step Feature (no Event, schedule, sleep, signal, or external s
3635
3637
  rejectedOrders: { type: 'number' },
3636
3638
  },
3637
3639
  handler: async (ctx) => {
3638
- const validated = await ctx.step('validate-orders', async () => {
3639
- return validateOrders(ctx.input);
3640
- });
3641
- const resolved = await ctx.step('resolve-orders', async () => {
3642
- return resolveOrders(validated);
3643
- });
3644
- return ctx.step('build-result', async () => {
3645
- return buildResult(resolved);
3646
- });
3640
+ const validated = await ctx.step('validate-orders', () =>
3641
+ ctx.database.execute({ database: 'orders-db', event: 'validate-orders', input: ctx.input }));
3642
+ return ctx.step('resolve-orders', () =>
3643
+ ctx.database.execute({ database: 'orders-db', event: 'resolve-orders', input: validated }));
3647
3644
  },
3648
3645
  });
3649
3646
  This is a valid Feature despite requiring no signal and producing no Event. Its qualification comes
@@ -3652,7 +3649,7 @@ step-level execution history.
3652
3649
 
3653
3650
  Define a feature (write this into the project's source files — do NOT use features.create):
3654
3651
  // src/features/onboard-user.ts (or the equivalent path/language for the project)
3655
- await ductape.features.define({
3652
+ await ductape.feature.define({
3656
3653
  tag: "onboard-user",
3657
3654
  name: "Onboard User",
3658
3655
  handler: async (ctx) => {
@@ -4699,6 +4696,195 @@ DEPRECATED ALIASES
4699
4696
  useWorkflowCancel → useFeatureCancel
4700
4697
  `.trim(),
4701
4698
  };
4699
+ const GRAPH_VECTOR_PROJECTION_GUIDANCE = `
4700
+
4701
+ ━━━ EVENT → GRAPH/VECTOR PROJECTION CONTRACT (CURRENT TYPESCRIPT/NESTJS SDK) ━━━
4702
+
4703
+ SOURCE-VERIFIED CODE-FIRST SUPPORT:
4704
+ IFeatureContext and RecordingContext expose both ctx.graph and ctx.vector. Their calls compile to
4705
+ portable JSON steps and the FeatureExecutor runs those steps against the configured local graph or
4706
+ vector service. Keep every primitive call inside ctx.step().
4707
+
4708
+ The TypeScript SDK facade is singular: ductape.feature.define/execute/dispatch. The backend proxy
4709
+ module is named "features", but application code must not infer a ductape.features property.
4710
+
4711
+ EXACT CODE-FIRST GRAPH CONTEXT:
4712
+ ctx.graph.createNode<T>({ graph, labels: string[], properties })
4713
+ ctx.graph.updateNode<T>({ graph, id: string|number, properties })
4714
+ ctx.graph.deleteNode({ graph, id })
4715
+ ctx.graph.createRelationship<T>({ graph, from, to, type, properties? })
4716
+ ctx.graph.deleteRelationship({ graph, id })
4717
+ ctx.graph.query<T>({ graph, action, params? })
4718
+ ctx.graph.execute<T>({ graph, action, input })
4719
+ Every call must be inside ctx.step(). Prefer the direct ctx.graph primitive that expresses the
4720
+ operation. Saved actions are not generally preferred over createNode/updateNode/deleteNode,
4721
+ relationship operations, or query; use a saved action only for a reusable parameterized operation
4722
+ that the typed primitives cannot express. createNode/createRelationship are not projection upserts.
4723
+
4724
+ SAVED GRAPH ACTION ASSET AND CLI CONTRACT:
4725
+ Canonical project path:
4726
+ ductape/graphs/<graph-tag>/actions/<action-tag>.action.json
4727
+ action-tag is generated by lowercasing name, replacing non-alphanumerics with hyphens, and trimming
4728
+ hyphens. Exactly one action object belongs in each file. Allowed top-level fields are graphTag,
4729
+ name, description, operation, query, and parameters. Required fields are graphTag, name, operation,
4730
+ query, and parameters. There is no persisted version field.
4731
+
4732
+ Example ductape/graphs/discovery-graph/actions/upsert-product-v1.action.json:
4733
+ {
4734
+ "graphTag": "discovery-graph",
4735
+ "name": "Upsert Product V1",
4736
+ "description": "Idempotently project one product by stable domain ID",
4737
+ "operation": "executeRaw",
4738
+ "query": {
4739
+ "operation": "executeRaw",
4740
+ "options": {
4741
+ "query": "MERGE (p:Product {id: $productId}) SET p.name = $name, p.sourceVersion = $sourceVersion RETURN p",
4742
+ "params": { "productId": null, "name": null, "sourceVersion": null }
4743
+ }
4744
+ },
4745
+ "parameters": [
4746
+ { "name": "productId", "path": "options.params.productId", "defaultValue": null, "type": "string", "required": true },
4747
+ { "name": "name", "path": "options.params.name", "defaultValue": null, "type": "string", "required": true },
4748
+ { "name": "sourceVersion", "path": "options.params.sourceVersion", "defaultValue": 0, "type": "number", "required": true }
4749
+ ]
4750
+ }
4751
+ Validate without mutation:
4752
+ ductape graph validateAction -f ductape/graphs/discovery-graph/actions/upsert-product-v1.action.json --json
4753
+ Create and persist against the linked product:
4754
+ ductape graph createAction -f ductape/graphs/discovery-graph/actions/upsert-product-v1.action.json --json
4755
+ Read back with graph.getAction/listActions before relying on it. In-place updates use:
4756
+ ductape graph updateAction -f <patch.json> --json
4757
+ where patch.json is { "actionTag":"upsert-product-v1", "graphTag":"discovery-graph", "updates":{...} }.
4758
+ Updating name does not change the stored tag. For breaking query/input/result changes, create a new
4759
+ name/tag such as V2 and migrate callers; use updateAction for compatible corrections only.
4760
+ ctx.graph.execute returns IExecuteGraphActionResult<T>: { success, executionTime, data: T[], count,
4761
+ error? }. It resolves parameter values by each parameter.path. The SDK currently returns
4762
+ success:false for action execution errors, so Feature code must inspect success and throw to fail
4763
+ the step; do not treat a resolved unsuccessful result as success.
4764
+
4765
+ EXACT CODE-FIRST VECTOR CONTEXT:
4766
+ ctx.vector.upsert<T>({
4767
+ vector: "product-vectors",
4768
+ vectors: [{ id, values: number[], metadata? }], namespace?, wait?
4769
+ });
4770
+ ctx.vector.upsertOne<T>({
4771
+ vector: "product-vectors", id, values: number[], metadata?, namespace?
4772
+ });
4773
+ ctx.vector.query<T>({
4774
+ vector: "product-vectors", values: number[], topK: number,
4775
+ namespace?, filter?, includeValues?, includeMetadata?, minScore?
4776
+ });
4777
+ ctx.vector.deleteVectors<T>({ vector: "product-vectors", ids?, namespace?, deleteAll?, filter? });
4778
+ ctx.vector.execute<T>({ vector: "product-vectors", action: "saved-action", input });
4779
+ Product/env and the Feature's inherited session are supplied by the executor. Do not serialize
4780
+ them into the Feature definition. Note the deliberate naming: ctx.vector.query uses values for
4781
+ the embedding, while the lower-level ductape.vector.query runtime facade uses vector.
4782
+ Vertex AI Vector Search stores and searches supplied vectors. It does NOT generate embeddings.
4783
+ Do not claim that a Vertex Vector Search index embeds text.
4784
+
4785
+ EMBEDDING EXECUTION — CURRENT CODE-FIRST CONTRACT:
4786
+ Registered product models are configuration CRUD only in the current SDK. There is no ctx.model,
4787
+ ctx.models, ctx.ai, or registered-model execute method, so never generate those calls. To execute
4788
+ discovery-embeddings portably inside a Feature, define it as a portable function contract (preferred
4789
+ for application-owned embedding code) or call an already registered App action via ctx.api.run.
4790
+
4791
+ import { defineFunctions } from '@ductape/sdk';
4792
+ type EmbedInput = { texts: string[] };
4793
+ type EmbedOutput = { embeddings: number[][]; dimensions: number; model: string };
4794
+ export const DiscoveryEmbeddings = defineFunctions({
4795
+ namespace: 'discovery-embeddings', version: '1', operations: { embed: {
4796
+ input: { type:'object', required:['texts'], additionalProperties:false,
4797
+ properties:{ texts:{ type:'array', items:{ type:'string', minLength:1 } } } },
4798
+ output: { type:'object', required:['embeddings','dimensions','model'], additionalProperties:false,
4799
+ properties:{ embeddings:{ type:'array', items:{ type:'array', items:{ type:'number' } } },
4800
+ dimensions:{ type:'integer', minimum:1 }, model:{ type:'string', minLength:1 } } },
4801
+ timeout_ms: 15000, idempotent: true, transports:[{ type:'local' }]
4802
+ } } });
4803
+
4804
+ In the Feature handler:
4805
+ const embeddings = ctx.functions.use(DiscoveryEmbeddings);
4806
+ const generated = await ctx.step('embed-products', () =>
4807
+ embeddings.embed({ texts: ctx.input.texts }));
4808
+ if (generated.embeddings.length !== ctx.input.texts.length ||
4809
+ generated.embeddings.some(v => v.length !== EXPECTED_VECTOR_DIMENSIONS)) {
4810
+ throw new Error('EMBEDDING_DIMENSION_MISMATCH');
4811
+ }
4812
+ Register a real local handler at application bootstrap or configure a signed HTTPS/events transport.
4813
+ Input and output JSON schemas are validated at runtime. Missing implementation fails with
4814
+ FUNCTION_UNAVAILABLE; schema mismatch fails with FUNCTION_SCHEMA_VALIDATION_FAILED; timeout fails
4815
+ with FUNCTION_TIMEOUT. Never substitute sample/random/recording-time embeddings. The Feature's
4816
+ session is inherited in the portable function context, outside its business input.
4817
+
4818
+ NESTJS CONSUMER ROUTING AND FEATURE INPUT:
4819
+ @Events.Consumer receives the published message object. Ductape may add
4820
+ __ductape_message_id; there is no automatic domain-event envelope and no automatic session token.
4821
+ DuctapeModule subscribes at startup using product/env defaults unless decorator overrides are set.
4822
+ The callback is acknowledged after it returns successfully; throwing propagates failure to the
4823
+ provider's retry/nack path. Do not catch and suppress a failed projection.
4824
+
4825
+ import { Injectable } from '@nestjs/common';
4826
+ import { DuctapeContextService, Events } from '@ductape/nestjs';
4827
+
4828
+ type ProductProjectionEvent = {
4829
+ eventId: string; // stable across retries and replay
4830
+ productId: string;
4831
+ version: number; // monotonic aggregate/source version
4832
+ name: string;
4833
+ embedding: number[]; // only if produced upstream; Vector Search does not generate it
4834
+ __ductape_message_id?: string;
4835
+ };
4836
+
4837
+ @Injectable()
4838
+ export class ProductProjectionConsumer {
4839
+ constructor(private readonly ductape: DuctapeContextService) {}
4840
+
4841
+ @Events.Consumer({ event: 'catalog-events:product-projection-requested' })
4842
+ async project(message: ProductProjectionEvent): Promise<void> {
4843
+ const result = await this.ductape.sdk.feature.execute({
4844
+ product: 'buydeck',
4845
+ env: 'snd',
4846
+ tag: 'project-product-graph',
4847
+ input: message,
4848
+ idempotency_key: 'product-projection:' + message.eventId,
4849
+ });
4850
+ if (result.status !== 'completed') {
4851
+ throw new Error(result.error ?? 'Product graph projection did not complete');
4852
+ }
4853
+ }
4854
+ }
4855
+
4856
+ The consumer passes its message explicitly as Feature input. Use a stable domain eventId for the
4857
+ Feature idempotency key; __ductape_message_id is transport tracking metadata and must not replace a
4858
+ domain idempotency key. Broker replay/redelivery means consumers and every projection write must
4859
+ remain independently idempotent.
4860
+
4861
+ ORDERING, PROJECTION STATE, AND FAILURE RECOVERY:
4862
+ There is no atomic transaction spanning MongoDB, Neo4j, and Vertex Vector Search. Never claim that
4863
+ ctx.step ordering creates a distributed transaction. Treat MongoDB as the canonical source and
4864
+ graph/vector stores as rebuildable projections.
4865
+
4866
+ Recommended state machine, keyed by projection name + aggregate ID + source version/event ID:
4867
+ 1. Atomically claim/read projection_state in MongoDB; completed means return success.
4868
+ 2. Apply an idempotent Neo4j MERGE using stable domain IDs and sourceVersion.
4869
+ 3. Upsert the vector using the same stable ID and sourceVersion metadata.
4870
+ 4. Mark projection_state completed LAST, with graph/vector versions, checksum, and timestamps.
4871
+ On failure, retain pending/failed state and throw so the broker retries. A retry repeats graph/vector
4872
+ upserts safely and only then marks completion. Do not mark Mongo completion before derived writes.
4873
+ If strict atomic visibility across all three stores is required, the platform does not provide it.
4874
+
4875
+ REBUILD AND DRIFT REPAIR:
4876
+ Rebuild from canonical MongoDB records, not from broker retention or processor logs. Use a named,
4877
+ versioned rebuild job/Feature that pages deterministically, performs the same idempotent graph/vector
4878
+ upserts, checkpoints the last canonical cursor, and records projection version/checksum. Drift repair
4879
+ compares canonical IDs/sourceVersion/checksum with both projections, repairs missing/stale entries,
4880
+ and removes extras only after a complete successful scan. Keep live consumer and rebuild writes
4881
+ monotonic so an older rebuild item cannot overwrite a newer projection.
4882
+
4883
+ IMPLEMENTATION GATE:
4884
+ Combined database + graph + vector code-first projection Features are supported. Still stop when
4885
+ an embedding source, vector dimension, saved graph/vector action, or idempotency strategy is not
4886
+ specified; do not invent those application-level contracts.
4887
+ `.trim();
4702
4888
  const docsHandler = async (args) => {
4703
4889
  const requested = args.topic.toLowerCase().trim();
4704
4890
  const recoveryAliases = /^(dlq|dead[- ]?letters?|failed messages?|retry|retries|poison messages?|replay|reprocess|consumer failures?)$/;
@@ -4719,7 +4905,10 @@ const docsHandler = async (args) => {
4719
4905
  canonicalDocumentationTopic: 'events',
4720
4906
  }, null, 2)}`
4721
4907
  : '';
4722
- return { content: [{ type: 'text', text: doc + structured }] };
4908
+ const projectionGuidance = ['features', 'events', 'graphs', 'vector'].includes(key)
4909
+ ? `\n\n${GRAPH_VECTOR_PROJECTION_GUIDANCE}`
4910
+ : '';
4911
+ return { content: [{ type: 'text', text: doc + structured + projectionGuidance }] };
4723
4912
  };
4724
4913
  const portableFunctionSetupHandler = async (args) => {
4725
4914
  const parsed = new URL(args.base_url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.14",
3
+ "version": "0.2.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",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs && node scripts/check-project-link-guidance.mjs",
18
+ "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },