@firfi/huly-mcp 0.15.0 → 0.16.0

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/README.md +4 -2
  2. package/dist/index.cjs +792 -266
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -406,9 +406,11 @@ For a Smithery publish schema example, see [docs/SMITHERY_URL_PUBLISH.md](docs/S
406
406
  | Tool | Description |
407
407
  |------|-------------|
408
408
  | `list_associations` | List Huly association definitions: class-level typed links that define which document classes may be related. Use this before create_relation to discover association IDs, source/target classes, and whether relation writes are supported. |
409
+ | `create_association` | Idempotently create one Huly association definition between two non-system classes. Use sourceClass/targetClass with sourceRole/targetRole and cardinality; returns an existing identical association by default. |
410
+ | `delete_association` | Idempotently delete one Huly association definition only when no concrete relations reference it. If relations exist, delete_relation must clean them up first; deleting an already-missing association is a successful no-op. |
409
411
  | `list_relations` | List concrete Huly relation instances under an association, optionally filtered by source and target documents. Requires at least one filter to avoid broad workspace scans. |
410
- | `create_relation` | Idempotently create one concrete relation between two resolved documents. Only succeeds for associations where list_associations reports canCreateRelation=true; otherwise it fails clearly. This build currently reports no generic associations as writable until a write allowlist is live-validated. |
411
- | `delete_relation` | Idempotently delete one concrete relation by relation ID or by exact association/source/target triple. Only succeeds for associations where list_associations reports canDeleteRelation=true; otherwise it fails clearly. This build currently reports no generic associations as writable until a write allowlist is live-validated. |
412
+ | `create_relation` | Idempotently create one concrete relation between two resolved documents for a writable association. Enforces association endpoint classes, direction, duplicate handling, automation-only restrictions, and cardinality. |
413
+ | `delete_relation` | Idempotently delete one concrete relation by relation ID or by exact association/source/target triple. Triple deletes use the same direction semantics as create_relation and fail if the selector is ambiguous. |
412
414
 
413
415
  ### Activity
414
416
 
package/dist/index.cjs CHANGED
@@ -151353,12 +151353,305 @@ var InvalidContentTypeError = class extends Schema_exports.TaggedError()(
151353
151353
  }
151354
151354
  };
151355
151355
 
151356
+ // src/domain/schemas/generic-associations.ts
151357
+ var AssociationIdentifier = NonEmptyString2.pipe(Schema_exports.brand("AssociationIdentifier"));
151358
+ var AssociationName = NonEmptyString2.pipe(Schema_exports.brand("AssociationName"));
151359
+ var AssociationRoleName = NonEmptyString2.pipe(Schema_exports.brand("AssociationRoleName"));
151360
+ var RelationIdentifier = NonEmptyString2.pipe(Schema_exports.brand("RelationIdentifier"));
151361
+ var ListRelationsWarning = NonEmptyString2.pipe(Schema_exports.brand("ListRelationsWarning"));
151362
+ var CardinalityValues = [
151363
+ "one-to-one",
151364
+ "one-to-many",
151365
+ "many-to-many"
151366
+ ];
151367
+ var CardinalitySchema = Schema_exports.Literal(...CardinalityValues).annotations({
151368
+ description: `Association cardinality: ${enumValuesDescription(CardinalityValues)}`
151369
+ });
151370
+ var RelationDirectionValues = ["source-to-target", "target-to-source", "either"];
151371
+ var RelationDirectionSchema = Schema_exports.Literal(...RelationDirectionValues);
151372
+ var DefaultRelationDirection = "source-to-target";
151373
+ var relationDirectionDescription = `Relation traversal direction: ${enumValuesDescription(RelationDirectionValues)}. Defaults to ${DefaultRelationDirection}.`;
151374
+ var RelationIfExistsSchema = Schema_exports.Literal("return_existing", "fail");
151375
+ var AssociationIfExistsSchema = Schema_exports.Literal("return_existing", "fail");
151376
+ var RawObjectLocatorSchema = Schema_exports.Struct({
151377
+ kind: Schema_exports.Literal("raw"),
151378
+ id: DocId.annotations({
151379
+ description: "Raw Huly document _id"
151380
+ }),
151381
+ class: Schema_exports.optional(ObjectClassName.annotations({
151382
+ description: "Raw Huly document class, such as tracker:class:Issue. Required unless the association side determines the expected class."
151383
+ }))
151384
+ });
151385
+ var IssueObjectLocatorSchema = Schema_exports.Struct({
151386
+ kind: Schema_exports.Literal("issue"),
151387
+ issue: IssueIdentifier.annotations({
151388
+ description: "Issue identifier, such as HULY-123, or a numeric issue number when project is also provided."
151389
+ }),
151390
+ project: Schema_exports.optional(ProjectIdentifier.annotations({
151391
+ description: "Project identifier. Optional when issue already includes a project prefix like HULY-123."
151392
+ }))
151393
+ });
151394
+ var DocumentObjectLocatorSchema = Schema_exports.Struct({
151395
+ kind: Schema_exports.Literal("document"),
151396
+ document: DocumentIdentifier.annotations({
151397
+ description: "Document title or ID"
151398
+ }),
151399
+ teamspace: Schema_exports.optional(TeamspaceIdentifier.annotations({
151400
+ description: "Teamspace name or ID. If omitted, document title matches must be unique across the workspace."
151401
+ }))
151402
+ });
151403
+ var GenericObjectLocatorSchema = Schema_exports.Union(
151404
+ RawObjectLocatorSchema,
151405
+ IssueObjectLocatorSchema,
151406
+ DocumentObjectLocatorSchema
151407
+ ).annotations({
151408
+ title: "GenericObjectLocator",
151409
+ description: "Explicit locator for a Huly document endpoint. Use raw for known _id values, issue for tracker issues, or document for Huly documents. Card locators are intentionally not included until a robust card resolver is available."
151410
+ });
151411
+ var ResolvedObjectSummarySchema = Schema_exports.Struct({
151412
+ id: DocId,
151413
+ class: ObjectClassName,
151414
+ display: NonEmptyString2,
151415
+ locatorKind: Schema_exports.Literal("raw", "issue", "document"),
151416
+ warning: Schema_exports.optional(Schema_exports.String)
151417
+ });
151418
+ var AssociationSummarySchema = Schema_exports.Struct({
151419
+ associationId: AssociationId,
151420
+ name: Schema_exports.optional(AssociationName),
151421
+ label: Schema_exports.optional(NonEmptyString2),
151422
+ description: Schema_exports.optional(Schema_exports.String),
151423
+ sourceClass: ObjectClassName,
151424
+ sourceClassLabel: Schema_exports.optional(NonEmptyString2.annotations({
151425
+ description: "Best-effort human display label for sourceClass when the class is known to this server"
151426
+ })),
151427
+ targetClass: ObjectClassName,
151428
+ targetClassLabel: Schema_exports.optional(NonEmptyString2.annotations({
151429
+ description: "Best-effort human display label for targetClass when the class is known to this server"
151430
+ })),
151431
+ sourceRole: Schema_exports.optional(AssociationRoleName),
151432
+ targetRole: Schema_exports.optional(AssociationRoleName),
151433
+ relationClass: Schema_exports.optional(ObjectClassName),
151434
+ cardinality: CardinalitySchema,
151435
+ symmetric: Schema_exports.Boolean,
151436
+ system: Schema_exports.Boolean,
151437
+ canListRelations: Schema_exports.Boolean,
151438
+ canCreateRelation: Schema_exports.Boolean,
151439
+ canDeleteRelation: Schema_exports.Boolean,
151440
+ unsupportedReason: Schema_exports.optional(Schema_exports.String)
151441
+ });
151442
+ var RelationSummarySchema = Schema_exports.Struct({
151443
+ relationId: RelationId,
151444
+ associationId: AssociationId,
151445
+ associationName: Schema_exports.optional(AssociationName),
151446
+ source: ResolvedObjectSummarySchema,
151447
+ target: ResolvedObjectSummarySchema,
151448
+ createdOn: Schema_exports.optional(Timestamp),
151449
+ modifiedOn: Schema_exports.optional(Timestamp)
151450
+ });
151451
+ var ListAssociationsParamsSchema = Schema_exports.Struct({
151452
+ association: Schema_exports.optional(AssociationIdentifier.annotations({
151453
+ description: "Association _id, source/target role name, or stable association name"
151454
+ })),
151455
+ sourceClass: Schema_exports.optional(ObjectClassName.annotations({
151456
+ description: "Only return associations whose source class matches this Huly class ID"
151457
+ })),
151458
+ targetClass: Schema_exports.optional(ObjectClassName.annotations({
151459
+ description: "Only return associations whose target class matches this Huly class ID"
151460
+ })),
151461
+ writableOnly: Schema_exports.optional(Schema_exports.Boolean.annotations({
151462
+ description: "Only return associations whose relation create/delete path has been validated and allowlisted"
151463
+ })),
151464
+ includeSystem: Schema_exports.optional(Schema_exports.Boolean.annotations({
151465
+ description: "Include internal/system associations. Defaults to false."
151466
+ })),
151467
+ limit: Schema_exports.optional(LimitParam.annotations({
151468
+ description: "Maximum number of associations to return (default: 50)"
151469
+ }))
151470
+ }).annotations({
151471
+ title: "ListAssociationsParams",
151472
+ description: "Parameters for listing generic Huly association definitions"
151473
+ });
151474
+ var ListAssociationsResultSchema = Schema_exports.Struct({
151475
+ associations: Schema_exports.Array(AssociationSummarySchema),
151476
+ total: Schema_exports.Number
151477
+ });
151478
+ var CreateAssociationParamsSchema = Schema_exports.Struct({
151479
+ sourceClass: ObjectClassName.annotations({
151480
+ description: "Source Huly class ID, such as tracker:class:Issue. core:class:* system classes are rejected."
151481
+ }),
151482
+ targetClass: ObjectClassName.annotations({
151483
+ description: "Target Huly class ID, such as tracker:class:Issue. core:class:* system classes are rejected."
151484
+ }),
151485
+ sourceRole: AssociationRoleName.annotations({
151486
+ description: "Role name stored on the source side of the association."
151487
+ }),
151488
+ targetRole: AssociationRoleName.annotations({
151489
+ description: "Role name stored on the target side of the association."
151490
+ }),
151491
+ cardinality: CardinalitySchema,
151492
+ automationOnly: Schema_exports.optional(Schema_exports.Boolean.annotations({
151493
+ description: "Whether Huly automation-only UI paths should own relation writes for this association. Defaults to false."
151494
+ })),
151495
+ ifExists: Schema_exports.optional(AssociationIfExistsSchema.annotations({
151496
+ description: "return_existing (default) returns an identical existing association; fail reports an existing association as an error"
151497
+ }))
151498
+ }).annotations({
151499
+ title: "CreateAssociationParams",
151500
+ description: "Parameters for idempotently creating a Huly association definition in the model space. The created association can then be used with create_relation."
151501
+ });
151502
+ var CreateAssociationResultSchema = Schema_exports.Struct({
151503
+ association: AssociationSummarySchema,
151504
+ created: Schema_exports.Boolean,
151505
+ existing: Schema_exports.Boolean
151506
+ });
151507
+ var DeleteAssociationParamsSchema = Schema_exports.Struct({
151508
+ association: AssociationIdentifier.annotations({
151509
+ description: "Association _id or unambiguous name returned by list_associations. Deleting a missing association is a successful no-op."
151510
+ })
151511
+ }).annotations({
151512
+ title: "DeleteAssociationParams",
151513
+ description: "Parameters for idempotently deleting a Huly association definition. The association must have zero concrete relations."
151514
+ });
151515
+ var DeleteAssociationResultSchema = Schema_exports.Struct({
151516
+ association: AssociationIdentifier,
151517
+ associationId: Schema_exports.optional(AssociationId),
151518
+ deleted: Schema_exports.Boolean,
151519
+ relationCount: Schema_exports.Number,
151520
+ reason: Schema_exports.optional(Schema_exports.Literal("not_found", "deleted"))
151521
+ });
151522
+ var ListRelationsParamsBaseSchema = Schema_exports.Struct({
151523
+ association: Schema_exports.optional(AssociationIdentifier.annotations({
151524
+ description: "Association _id or name. If omitted, relations are listed only across supported visible associations."
151525
+ })),
151526
+ source: Schema_exports.optional(GenericObjectLocatorSchema.annotations({
151527
+ description: "Optional source endpoint filter"
151528
+ })),
151529
+ target: Schema_exports.optional(GenericObjectLocatorSchema.annotations({
151530
+ description: "Optional target endpoint filter"
151531
+ })),
151532
+ direction: Schema_exports.optional(RelationDirectionSchema.annotations({
151533
+ description: relationDirectionDescription
151534
+ })),
151535
+ limit: Schema_exports.optional(LimitParam.annotations({
151536
+ description: "Maximum number of relations to return (default: 50)"
151537
+ }))
151538
+ }).annotations({
151539
+ title: "ListRelationsParams",
151540
+ description: "Parameters for listing concrete Huly relation instances"
151541
+ });
151542
+ var ListRelationsParamsSchema = ListRelationsParamsBaseSchema.pipe(
151543
+ Schema_exports.filter(
151544
+ (params) => params.association === void 0 && params.source === void 0 && params.target === void 0 ? "Provide at least one of association, source, or target to avoid broad workspace scans." : void 0
151545
+ )
151546
+ ).annotations({
151547
+ title: "ListRelationsParams",
151548
+ description: "Parameters for listing concrete Huly relation instances"
151549
+ });
151550
+ var ListRelationsResultSchema = Schema_exports.Struct({
151551
+ relations: Schema_exports.Array(RelationSummarySchema),
151552
+ total: Schema_exports.Number,
151553
+ warnings: Schema_exports.optional(
151554
+ Schema_exports.NonEmptyArray(ListRelationsWarning).annotations({
151555
+ description: "Non-fatal warnings about result completeness or resolution. Treat these as guidance for narrowing a follow-up call."
151556
+ })
151557
+ )
151558
+ });
151559
+ var CreateRelationParamsSchema = Schema_exports.Struct({
151560
+ association: AssociationIdentifier.annotations({
151561
+ description: "Association _id or unambiguous name returned by list_associations"
151562
+ }),
151563
+ source: GenericObjectLocatorSchema.annotations({
151564
+ description: "Source endpoint document"
151565
+ }),
151566
+ target: GenericObjectLocatorSchema.annotations({
151567
+ description: "Target endpoint document"
151568
+ }),
151569
+ direction: Schema_exports.optional(RelationDirectionSchema.annotations({
151570
+ description: relationDirectionDescription
151571
+ })),
151572
+ ifExists: Schema_exports.optional(RelationIfExistsSchema.annotations({
151573
+ description: "return_existing (default) returns an existing relation; fail reports an existing relation as an error"
151574
+ }))
151575
+ }).annotations({
151576
+ title: "CreateRelationParams",
151577
+ description: "Parameters for idempotently creating a concrete generic relation"
151578
+ });
151579
+ var CreateRelationResultSchema = Schema_exports.Struct({
151580
+ relationId: RelationId,
151581
+ associationId: AssociationId,
151582
+ source: ResolvedObjectSummarySchema,
151583
+ target: ResolvedObjectSummarySchema,
151584
+ created: Schema_exports.Boolean,
151585
+ existing: Schema_exports.Boolean
151586
+ });
151587
+ var DeleteRelationByIdParamsSchema = Schema_exports.Struct({
151588
+ relation: RelationIdentifier.annotations({
151589
+ description: "Concrete relation _id to delete"
151590
+ })
151591
+ }).annotations({
151592
+ title: "DeleteRelationByIdParams",
151593
+ description: "Delete one concrete relation by its relation ID."
151594
+ });
151595
+ var DeleteRelationByTripleParamsSchema = Schema_exports.Struct({
151596
+ association: AssociationIdentifier.annotations({
151597
+ description: "Association _id or unambiguous name"
151598
+ }),
151599
+ source: GenericObjectLocatorSchema.annotations({
151600
+ description: "Source endpoint"
151601
+ }),
151602
+ target: GenericObjectLocatorSchema.annotations({
151603
+ description: "Target endpoint"
151604
+ }),
151605
+ direction: Schema_exports.optional(RelationDirectionSchema.annotations({
151606
+ description: relationDirectionDescription
151607
+ }))
151608
+ }).annotations({
151609
+ title: "DeleteRelationByTripleParams",
151610
+ description: "Delete one concrete relation by exact association + source + target triple."
151611
+ });
151612
+ var DeleteRelationParamsSchema = Schema_exports.Union(
151613
+ DeleteRelationByIdParamsSchema,
151614
+ DeleteRelationByTripleParamsSchema
151615
+ ).annotations({
151616
+ title: "DeleteRelationParams",
151617
+ description: "Parameters for idempotently deleting one concrete generic relation. Provide either relation, or the full association + source + target triple."
151618
+ });
151619
+ var DeleteRelationResultSchema = Schema_exports.Struct({
151620
+ relationId: Schema_exports.optional(RelationId),
151621
+ associationId: Schema_exports.optional(AssociationId),
151622
+ deleted: Schema_exports.Boolean,
151623
+ reason: Schema_exports.optional(Schema_exports.Literal("not_found", "deleted"))
151624
+ });
151625
+ var listAssociationsParamsJsonSchema = JSONSchema_exports.make(ListAssociationsParamsSchema);
151626
+ var createAssociationParamsJsonSchema = JSONSchema_exports.make(CreateAssociationParamsSchema);
151627
+ var deleteAssociationParamsJsonSchema = JSONSchema_exports.make(DeleteAssociationParamsSchema);
151628
+ var listRelationsParamsJsonSchema = {
151629
+ ...JSONSchema_exports.make(ListRelationsParamsBaseSchema),
151630
+ anyOf: [
151631
+ { required: ["association"] },
151632
+ { required: ["source"] },
151633
+ { required: ["target"] }
151634
+ ]
151635
+ };
151636
+ var createRelationParamsJsonSchema = JSONSchema_exports.make(CreateRelationParamsSchema);
151637
+ var deleteRelationParamsJsonSchema = {
151638
+ ...JSONSchema_exports.make(DeleteRelationParamsSchema),
151639
+ type: "object"
151640
+ };
151641
+ var strictParseOptions = { onExcessProperty: "error" };
151642
+ var parseListAssociationsParams = Schema_exports.decodeUnknown(ListAssociationsParamsSchema, strictParseOptions);
151643
+ var parseCreateAssociationParams = Schema_exports.decodeUnknown(CreateAssociationParamsSchema, strictParseOptions);
151644
+ var parseDeleteAssociationParams = Schema_exports.decodeUnknown(DeleteAssociationParamsSchema, strictParseOptions);
151645
+ var parseListRelationsParams = Schema_exports.decodeUnknown(ListRelationsParamsSchema, strictParseOptions);
151646
+ var parseCreateRelationParams = Schema_exports.decodeUnknown(CreateRelationParamsSchema, strictParseOptions);
151647
+ var parseDeleteRelationParams = Schema_exports.decodeUnknown(DeleteRelationParamsSchema, strictParseOptions);
151648
+
151356
151649
  // src/huly/errors-generic-associations.ts
151357
151650
  var CandidateSchema = Schema_exports.Struct({
151358
151651
  id: AssociationId,
151359
- name: Schema_exports.optional(Schema_exports.String),
151360
- sourceClass: Schema_exports.optional(Schema_exports.String),
151361
- targetClass: Schema_exports.optional(Schema_exports.String)
151652
+ name: Schema_exports.optional(AssociationName),
151653
+ sourceClass: Schema_exports.optional(ObjectClassName),
151654
+ targetClass: Schema_exports.optional(ObjectClassName)
151362
151655
  });
151363
151656
  var formatCandidates = (candidates) => candidates.map((candidate) => {
151364
151657
  const name = candidate.name === void 0 ? "" : `${candidate.name} `;
@@ -151419,6 +151712,64 @@ var RelationMutationUnsupportedError = class extends Schema_exports.TaggedError(
151419
151712
  return `Generic relation mutation${id} is not supported: ${this.reason}. Call list_associations with writableOnly=true to discover writable associations.`;
151420
151713
  }
151421
151714
  };
151715
+ var AssociationSystemClassUnsupportedError = class extends Schema_exports.TaggedError()(
151716
+ "AssociationSystemClassUnsupportedError",
151717
+ {
151718
+ className: ObjectClassName,
151719
+ operation: Schema_exports.Literal("create_association", "delete_association", "create_relation", "delete_relation")
151720
+ }
151721
+ ) {
151722
+ get message() {
151723
+ return `${this.operation} does not support core system class '${this.className}' in generic association writes.`;
151724
+ }
151725
+ };
151726
+ var AssociationConflictError = class extends Schema_exports.TaggedError()(
151727
+ "AssociationConflictError",
151728
+ {
151729
+ associationId: AssociationId,
151730
+ reason: Schema_exports.String
151731
+ }
151732
+ ) {
151733
+ get message() {
151734
+ return `Association '${this.associationId}' already exists but conflicts with the requested definition: ${this.reason}.`;
151735
+ }
151736
+ };
151737
+ var AssociationInUseError = class extends Schema_exports.TaggedError()(
151738
+ "AssociationInUseError",
151739
+ {
151740
+ associationId: AssociationId,
151741
+ relationCount: Schema_exports.Number,
151742
+ sampleRelationIds: Schema_exports.Array(RelationId)
151743
+ }
151744
+ ) {
151745
+ get message() {
151746
+ const sample2 = this.sampleRelationIds.length === 0 ? "" : ` Sample relation IDs: ${this.sampleRelationIds.join(", ")}.`;
151747
+ return `Association '${this.associationId}' cannot be deleted because ${this.relationCount} relation(s) still reference it. Delete those relations first.${sample2}`;
151748
+ }
151749
+ };
151750
+ var RelationCardinalityViolationError = class extends Schema_exports.TaggedError()(
151751
+ "RelationCardinalityViolationError",
151752
+ {
151753
+ associationId: AssociationId,
151754
+ cardinality: CardinalitySchema,
151755
+ reason: Schema_exports.String
151756
+ }
151757
+ ) {
151758
+ get message() {
151759
+ return `Relation violates ${this.cardinality} cardinality for association '${this.associationId}': ${this.reason}.`;
151760
+ }
151761
+ };
151762
+ var RelationDirectionAmbiguousError = class extends Schema_exports.TaggedError()(
151763
+ "RelationDirectionAmbiguousError",
151764
+ {
151765
+ associationId: AssociationId,
151766
+ reason: Schema_exports.String
151767
+ }
151768
+ ) {
151769
+ get message() {
151770
+ return `Relation direction is ambiguous for association '${this.associationId}': ${this.reason}. Use source-to-target or target-to-source.`;
151771
+ }
151772
+ };
151422
151773
  var RelationEndpointClassMismatchError = class extends Schema_exports.TaggedError()(
151423
151774
  "RelationEndpointClassMismatchError",
151424
151775
  {
@@ -152274,9 +152625,14 @@ var HulyDomainError = Schema_exports.Union(
152274
152625
  ProcessExecutionNotCancellableError,
152275
152626
  AssociationNotFoundError,
152276
152627
  AssociationIdentifierAmbiguousError,
152628
+ AssociationSystemClassUnsupportedError,
152629
+ AssociationConflictError,
152630
+ AssociationInUseError,
152277
152631
  RelationNotFoundError,
152278
152632
  RelationIdentifierAmbiguousError,
152279
152633
  RelationMutationUnsupportedError,
152634
+ RelationCardinalityViolationError,
152635
+ RelationDirectionAmbiguousError,
152280
152636
  RelationEndpointClassMismatchError,
152281
152637
  GenericObjectIdentifierAmbiguousError,
152282
152638
  GenericObjectLocatorInvalidError,
@@ -172203,7 +172559,7 @@ var PostHog = class extends PostHogBackendClient {
172203
172559
  };
172204
172560
 
172205
172561
  // src/version.ts
172206
- var VERSION = true ? "0.15.0" : "0.0.0-dev";
172562
+ var VERSION = true ? "0.16.0" : "0.0.0-dev";
172207
172563
 
172208
172564
  // src/telemetry/posthog.ts
172209
172565
  var POSTHOG_API_KEY = "phc_TGfFqCGdnF0p68wuFzd5WSw1IsBvOJW0YgoMJDyZPjm";
@@ -172387,9 +172743,14 @@ var INVALID_PARAMS_TAGS = /* @__PURE__ */ new Set([
172387
172743
  "ProcessCardNotFoundError",
172388
172744
  "AssociationNotFoundError",
172389
172745
  "AssociationIdentifierAmbiguousError",
172746
+ "AssociationSystemClassUnsupportedError",
172747
+ "AssociationConflictError",
172748
+ "AssociationInUseError",
172390
172749
  "RelationNotFoundError",
172391
172750
  "RelationIdentifierAmbiguousError",
172392
172751
  "RelationMutationUnsupportedError",
172752
+ "RelationCardinalityViolationError",
172753
+ "RelationDirectionAmbiguousError",
172393
172754
  "RelationEndpointClassMismatchError",
172394
172755
  "GenericObjectIdentifierAmbiguousError",
172395
172756
  "GenericObjectLocatorInvalidError",
@@ -172458,6 +172819,40 @@ var toMcpResponse = (response) => {
172458
172819
  return wire;
172459
172820
  };
172460
172821
 
172822
+ // src/mcp/input-schema-compat.ts
172823
+ var ROOT_COMPOSITION_KEYS = /* @__PURE__ */ new Set(["anyOf", "oneOf", "allOf"]);
172824
+ var isRecord2 = (value3) => typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
172825
+ var isObjectSchema = (schema) => "type" in schema && schema.type === "object";
172826
+ var mergeObjectFields = (sources) => {
172827
+ const merged = sources.reduce(
172828
+ (acc, source) => isRecord2(source) ? { ...source, ...acc } : acc,
172829
+ {}
172830
+ );
172831
+ return Object.keys(merged).length > 0 ? merged : void 0;
172832
+ };
172833
+ var rootCompositionBranches = (schema) => [...ROOT_COMPOSITION_KEYS].flatMap((key) => {
172834
+ const branches = schema[key];
172835
+ return Array.isArray(branches) ? branches.filter(isRecord2) : [];
172836
+ });
172837
+ var schemaAndCompositionDescendants = (schema) => [
172838
+ schema,
172839
+ ...rootCompositionBranches(schema).flatMap(schemaAndCompositionDescendants)
172840
+ ];
172841
+ var mergedSchemaField = (schema, field) => mergeObjectFields(schemaAndCompositionDescendants(schema).map((branch) => branch[field]));
172842
+ var toClientCompatibleInputSchema = (schema) => {
172843
+ const rootFields = Object.fromEntries(
172844
+ Object.entries(schema).filter(([key]) => key !== "type" && !ROOT_COMPOSITION_KEYS.has(key))
172845
+ );
172846
+ const properties = mergedSchemaField(schema, "properties");
172847
+ const defs = mergedSchemaField(schema, "$defs");
172848
+ return {
172849
+ ...rootFields,
172850
+ type: "object",
172851
+ ...properties === void 0 ? {} : { properties },
172852
+ ...defs === void 0 ? {} : { $defs: defs }
172853
+ };
172854
+ };
172855
+
172461
172856
  // src/mcp/tool-output-schema.ts
172462
172857
  var defaultToolOutputSchema = {
172463
172858
  type: "object",
@@ -175928,242 +176323,6 @@ var calendarTools = [
175928
176323
  }
175929
176324
  ];
175930
176325
 
175931
- // src/domain/schemas/generic-associations.ts
175932
- var AssociationIdentifier = NonEmptyString2.pipe(Schema_exports.brand("AssociationIdentifier"));
175933
- var RelationIdentifier = NonEmptyString2.pipe(Schema_exports.brand("RelationIdentifier"));
175934
- var ListRelationsWarning = NonEmptyString2.pipe(Schema_exports.brand("ListRelationsWarning"));
175935
- var CardinalityValues = [
175936
- "one-to-one",
175937
- "one-to-many",
175938
- "many-to-many"
175939
- ];
175940
- var CardinalitySchema = Schema_exports.Literal(...CardinalityValues).annotations({
175941
- description: `Association cardinality: ${enumValuesDescription(CardinalityValues)}`
175942
- });
175943
- var RelationDirectionValues = ["source-to-target", "target-to-source", "either"];
175944
- var RelationDirectionSchema = Schema_exports.Literal(...RelationDirectionValues);
175945
- var DefaultRelationDirection = "source-to-target";
175946
- var relationDirectionDescription = `Relation traversal direction: ${enumValuesDescription(RelationDirectionValues)}. Defaults to ${DefaultRelationDirection}.`;
175947
- var RelationIfExistsSchema = Schema_exports.Literal("return_existing", "fail");
175948
- var RawObjectLocatorSchema = Schema_exports.Struct({
175949
- kind: Schema_exports.Literal("raw"),
175950
- id: DocId.annotations({
175951
- description: "Raw Huly document _id"
175952
- }),
175953
- class: Schema_exports.optional(ObjectClassName.annotations({
175954
- description: "Raw Huly document class, such as tracker:class:Issue. Required unless the association side determines the expected class."
175955
- }))
175956
- });
175957
- var IssueObjectLocatorSchema = Schema_exports.Struct({
175958
- kind: Schema_exports.Literal("issue"),
175959
- issue: IssueIdentifier.annotations({
175960
- description: "Issue identifier, such as HULY-123, or a numeric issue number when project is also provided."
175961
- }),
175962
- project: Schema_exports.optional(ProjectIdentifier.annotations({
175963
- description: "Project identifier. Optional when issue already includes a project prefix like HULY-123."
175964
- }))
175965
- });
175966
- var DocumentObjectLocatorSchema = Schema_exports.Struct({
175967
- kind: Schema_exports.Literal("document"),
175968
- document: DocumentIdentifier.annotations({
175969
- description: "Document title or ID"
175970
- }),
175971
- teamspace: Schema_exports.optional(TeamspaceIdentifier.annotations({
175972
- description: "Teamspace name or ID. If omitted, document title matches must be unique across the workspace."
175973
- }))
175974
- });
175975
- var GenericObjectLocatorSchema = Schema_exports.Union(
175976
- RawObjectLocatorSchema,
175977
- IssueObjectLocatorSchema,
175978
- DocumentObjectLocatorSchema
175979
- ).annotations({
175980
- title: "GenericObjectLocator",
175981
- description: "Explicit locator for a Huly document endpoint. Use raw for known _id values, issue for tracker issues, or document for Huly documents. Card locators are intentionally not included until a robust card resolver is available."
175982
- });
175983
- var ResolvedObjectSummarySchema = Schema_exports.Struct({
175984
- id: DocId,
175985
- class: ObjectClassName,
175986
- display: NonEmptyString2,
175987
- locatorKind: Schema_exports.Literal("raw", "issue", "document"),
175988
- warning: Schema_exports.optional(Schema_exports.String)
175989
- });
175990
- var AssociationSummarySchema = Schema_exports.Struct({
175991
- associationId: AssociationId,
175992
- name: Schema_exports.optional(NonEmptyString2),
175993
- label: Schema_exports.optional(NonEmptyString2),
175994
- description: Schema_exports.optional(Schema_exports.String),
175995
- sourceClass: ObjectClassName,
175996
- sourceClassLabel: Schema_exports.optional(NonEmptyString2.annotations({
175997
- description: "Best-effort human display label for sourceClass when the class is known to this server"
175998
- })),
175999
- targetClass: ObjectClassName,
176000
- targetClassLabel: Schema_exports.optional(NonEmptyString2.annotations({
176001
- description: "Best-effort human display label for targetClass when the class is known to this server"
176002
- })),
176003
- sourceRole: Schema_exports.optional(NonEmptyString2),
176004
- targetRole: Schema_exports.optional(NonEmptyString2),
176005
- relationClass: Schema_exports.optional(ObjectClassName),
176006
- cardinality: CardinalitySchema,
176007
- symmetric: Schema_exports.Boolean,
176008
- system: Schema_exports.Boolean,
176009
- canListRelations: Schema_exports.Boolean,
176010
- canCreateRelation: Schema_exports.Boolean,
176011
- canDeleteRelation: Schema_exports.Boolean,
176012
- unsupportedReason: Schema_exports.optional(Schema_exports.String)
176013
- });
176014
- var RelationSummarySchema = Schema_exports.Struct({
176015
- relationId: RelationId,
176016
- associationId: AssociationId,
176017
- associationName: Schema_exports.optional(NonEmptyString2),
176018
- source: ResolvedObjectSummarySchema,
176019
- target: ResolvedObjectSummarySchema,
176020
- createdOn: Schema_exports.optional(Timestamp),
176021
- modifiedOn: Schema_exports.optional(Timestamp)
176022
- });
176023
- var ListAssociationsParamsSchema = Schema_exports.Struct({
176024
- association: Schema_exports.optional(AssociationIdentifier.annotations({
176025
- description: "Association _id, source/target role name, or stable association name"
176026
- })),
176027
- sourceClass: Schema_exports.optional(ObjectClassName.annotations({
176028
- description: "Only return associations whose source class matches this Huly class ID"
176029
- })),
176030
- targetClass: Schema_exports.optional(ObjectClassName.annotations({
176031
- description: "Only return associations whose target class matches this Huly class ID"
176032
- })),
176033
- writableOnly: Schema_exports.optional(Schema_exports.Boolean.annotations({
176034
- description: "Only return associations whose relation create/delete path has been validated and allowlisted"
176035
- })),
176036
- includeSystem: Schema_exports.optional(Schema_exports.Boolean.annotations({
176037
- description: "Include internal/system associations. Defaults to false."
176038
- })),
176039
- limit: Schema_exports.optional(LimitParam.annotations({
176040
- description: "Maximum number of associations to return (default: 50)"
176041
- }))
176042
- }).annotations({
176043
- title: "ListAssociationsParams",
176044
- description: "Parameters for listing generic Huly association definitions"
176045
- });
176046
- var ListAssociationsResultSchema = Schema_exports.Struct({
176047
- associations: Schema_exports.Array(AssociationSummarySchema),
176048
- total: Schema_exports.Number
176049
- });
176050
- var ListRelationsParamsBaseSchema = Schema_exports.Struct({
176051
- association: Schema_exports.optional(AssociationIdentifier.annotations({
176052
- description: "Association _id or name. If omitted, relations are listed only across supported visible associations."
176053
- })),
176054
- source: Schema_exports.optional(GenericObjectLocatorSchema.annotations({
176055
- description: "Optional source endpoint filter"
176056
- })),
176057
- target: Schema_exports.optional(GenericObjectLocatorSchema.annotations({
176058
- description: "Optional target endpoint filter"
176059
- })),
176060
- direction: Schema_exports.optional(RelationDirectionSchema.annotations({
176061
- description: relationDirectionDescription
176062
- })),
176063
- limit: Schema_exports.optional(LimitParam.annotations({
176064
- description: "Maximum number of relations to return (default: 50)"
176065
- }))
176066
- }).annotations({
176067
- title: "ListRelationsParams",
176068
- description: "Parameters for listing concrete Huly relation instances"
176069
- });
176070
- var ListRelationsParamsSchema = ListRelationsParamsBaseSchema.pipe(
176071
- Schema_exports.filter(
176072
- (params) => params.association === void 0 && params.source === void 0 && params.target === void 0 ? "Provide at least one of association, source, or target to avoid broad workspace scans." : void 0
176073
- )
176074
- ).annotations({
176075
- title: "ListRelationsParams",
176076
- description: "Parameters for listing concrete Huly relation instances"
176077
- });
176078
- var ListRelationsResultSchema = Schema_exports.Struct({
176079
- relations: Schema_exports.Array(RelationSummarySchema),
176080
- total: Schema_exports.Number,
176081
- warnings: Schema_exports.optional(
176082
- Schema_exports.NonEmptyArray(ListRelationsWarning).annotations({
176083
- description: "Non-fatal warnings about result completeness or resolution. Treat these as guidance for narrowing a follow-up call."
176084
- })
176085
- )
176086
- });
176087
- var CreateRelationParamsSchema = Schema_exports.Struct({
176088
- association: AssociationIdentifier.annotations({
176089
- description: "Association _id or unambiguous name returned by list_associations"
176090
- }),
176091
- source: GenericObjectLocatorSchema.annotations({
176092
- description: "Source endpoint document"
176093
- }),
176094
- target: GenericObjectLocatorSchema.annotations({
176095
- description: "Target endpoint document"
176096
- }),
176097
- ifExists: Schema_exports.optional(RelationIfExistsSchema.annotations({
176098
- description: "return_existing (default) returns an existing relation; fail reports an existing relation as an error"
176099
- }))
176100
- }).annotations({
176101
- title: "CreateRelationParams",
176102
- description: "Parameters for idempotently creating a concrete generic relation"
176103
- });
176104
- var CreateRelationResultSchema = Schema_exports.Struct({
176105
- relationId: RelationId,
176106
- associationId: AssociationId,
176107
- source: ResolvedObjectSummarySchema,
176108
- target: ResolvedObjectSummarySchema,
176109
- created: Schema_exports.Boolean,
176110
- existing: Schema_exports.Boolean
176111
- });
176112
- var DeleteRelationByIdParamsSchema = Schema_exports.Struct({
176113
- relation: RelationIdentifier.annotations({
176114
- description: "Concrete relation _id to delete"
176115
- })
176116
- }).annotations({
176117
- title: "DeleteRelationByIdParams",
176118
- description: "Delete one concrete relation by its relation ID."
176119
- });
176120
- var DeleteRelationByTripleParamsSchema = Schema_exports.Struct({
176121
- association: AssociationIdentifier.annotations({
176122
- description: "Association _id or unambiguous name"
176123
- }),
176124
- source: GenericObjectLocatorSchema.annotations({
176125
- description: "Source endpoint"
176126
- }),
176127
- target: GenericObjectLocatorSchema.annotations({
176128
- description: "Target endpoint"
176129
- })
176130
- }).annotations({
176131
- title: "DeleteRelationByTripleParams",
176132
- description: "Delete one concrete relation by exact association + source + target triple."
176133
- });
176134
- var DeleteRelationParamsSchema = Schema_exports.Union(
176135
- DeleteRelationByIdParamsSchema,
176136
- DeleteRelationByTripleParamsSchema
176137
- ).annotations({
176138
- title: "DeleteRelationParams",
176139
- description: "Parameters for idempotently deleting one concrete generic relation. Provide either relation, or the full association + source + target triple."
176140
- });
176141
- var DeleteRelationResultSchema = Schema_exports.Struct({
176142
- relationId: Schema_exports.optional(RelationId),
176143
- associationId: Schema_exports.optional(AssociationId),
176144
- deleted: Schema_exports.Boolean,
176145
- reason: Schema_exports.optional(Schema_exports.Literal("not_found", "deleted"))
176146
- });
176147
- var listAssociationsParamsJsonSchema = JSONSchema_exports.make(ListAssociationsParamsSchema);
176148
- var listRelationsParamsJsonSchema = {
176149
- ...JSONSchema_exports.make(ListRelationsParamsBaseSchema),
176150
- anyOf: [
176151
- { required: ["association"] },
176152
- { required: ["source"] },
176153
- { required: ["target"] }
176154
- ]
176155
- };
176156
- var createRelationParamsJsonSchema = JSONSchema_exports.make(CreateRelationParamsSchema);
176157
- var deleteRelationParamsJsonSchema = {
176158
- ...JSONSchema_exports.make(DeleteRelationParamsSchema),
176159
- type: "object"
176160
- };
176161
- var strictParseOptions = { onExcessProperty: "error" };
176162
- var parseListAssociationsParams = Schema_exports.decodeUnknown(ListAssociationsParamsSchema, strictParseOptions);
176163
- var parseListRelationsParams = Schema_exports.decodeUnknown(ListRelationsParamsSchema, strictParseOptions);
176164
- var parseCreateRelationParams = Schema_exports.decodeUnknown(CreateRelationParamsSchema, strictParseOptions);
176165
- var parseDeleteRelationParams = Schema_exports.decodeUnknown(DeleteRelationParamsSchema, strictParseOptions);
176166
-
176167
176326
  // src/domain/schemas/projects.ts
176168
176327
  var ProjectSummarySchema = Schema_exports.Struct({
176169
176328
  identifier: ProjectIdentifier,
@@ -182081,7 +182240,6 @@ var documentTools = [
182081
182240
 
182082
182241
  // src/huly/operations/generic-associations.ts
182083
182242
  var import_core35 = __toESM(require_lib4(), 1);
182084
- var WRITE_UNSUPPORTED_REASON = "no generic association relation write path has been live-validated for this workspace";
182085
182243
  var ASSOCIATION_DISCOVERY_LIMIT = 200;
182086
182244
  var ASSOCIATION_DISCOVERY_LIMIT_WARNING = ListRelationsWarning.make(
182087
182245
  `Association discovery reached the local ${ASSOCIATION_DISCOVERY_LIMIT}-association cap for at least one endpoint orientation. Huly did not indicate whether more matching associations exist, so list_relations may omit older matching associations; pass a specific association from list_associations to avoid this discovery cap.`
@@ -182097,8 +182255,7 @@ var VISIBLE_ASSOCIATION_FILTERS = {
182097
182255
  sourceClass: void 0,
182098
182256
  targetClass: void 0
182099
182257
  };
182100
- var associationName = (association) => association.nameA === association.nameB ? association.nameA : `${association.nameA} -> ${association.nameB}`;
182101
- var optionalNonEmpty = (value3) => value3 === void 0 ? void 0 : NonEmptyString2.make(value3);
182258
+ var associationName = (association) => association.nameA === association.nameB ? AssociationName.make(association.nameA) : AssociationName.make(`${association.nameA} -> ${association.nameB}`);
182102
182259
  var classLabelEntry = (classRef2, label) => [
182103
182260
  ObjectClassName.make(classRef2),
182104
182261
  NonEmptyString2.make(label)
@@ -182116,7 +182273,19 @@ var KNOWN_CLASS_LABELS = new Map([
182116
182273
  classLabelEntry(tracker.class.Milestone, "Milestone")
182117
182274
  ]);
182118
182275
  var classLabel = (classRef2) => KNOWN_CLASS_LABELS.get(classRef2);
182119
- var isSystemAssociation = (association) => String(association.classA).startsWith("core:class:") || String(association.classB).startsWith("core:class:");
182276
+ var isSystemClassName = (className) => className.startsWith("core:class:");
182277
+ var isSystemAssociation = (association) => isSystemClassName(String(association.classA)) || isSystemClassName(String(association.classB));
182278
+ var associationAutomationOnly = (association) => association.automationOnly === true;
182279
+ var relationWriteUnsupportedReason = (association) => {
182280
+ if (isSystemAssociation(association)) {
182281
+ return "association uses a core:class:* system class";
182282
+ }
182283
+ if (associationAutomationOnly(association)) {
182284
+ return "association is automation-only";
182285
+ }
182286
+ return void 0;
182287
+ };
182288
+ var isRelationWritableAssociation = (association) => relationWriteUnsupportedReason(association) === void 0;
182120
182289
  var isSymmetric = (association) => association.classA === association.classB && association.nameA === association.nameB;
182121
182290
  var ASSOCIATION_CARDINALITY = {
182122
182291
  "1:1": "one-to-one",
@@ -182126,33 +182295,39 @@ var ASSOCIATION_CARDINALITY = {
182126
182295
  var exactCardinalityMapping = (value3) => value3;
182127
182296
  exactCardinalityMapping(true);
182128
182297
  var cardinality = (type) => ASSOCIATION_CARDINALITY[type];
182298
+ var SDK_CARDINALITY = {
182299
+ "one-to-one": "1:1",
182300
+ "one-to-many": "1:N",
182301
+ "many-to-many": "N:N"
182302
+ };
182129
182303
  var toAssociationSummary = (association) => {
182130
182304
  const sourceClass = ObjectClassName.make(association.classA);
182131
182305
  const targetClass = ObjectClassName.make(association.classB);
182306
+ const unsupportedReason = relationWriteUnsupportedReason(association);
182132
182307
  return {
182133
182308
  associationId: AssociationId.make(association._id),
182134
- name: optionalNonEmpty(associationName(association)),
182309
+ name: associationName(association),
182135
182310
  sourceClass,
182136
182311
  sourceClassLabel: classLabel(sourceClass),
182137
182312
  targetClass,
182138
182313
  targetClassLabel: classLabel(targetClass),
182139
- sourceRole: NonEmptyString2.make(association.nameA),
182140
- targetRole: NonEmptyString2.make(association.nameB),
182314
+ sourceRole: AssociationRoleName.make(association.nameA),
182315
+ targetRole: AssociationRoleName.make(association.nameB),
182141
182316
  relationClass: ObjectClassName.make(core.class.Relation),
182142
182317
  cardinality: cardinality(association.type),
182143
182318
  symmetric: isSymmetric(association),
182144
182319
  system: isSystemAssociation(association),
182145
182320
  canListRelations: true,
182146
- canCreateRelation: false,
182147
- canDeleteRelation: false,
182148
- unsupportedReason: WRITE_UNSUPPORTED_REASON
182321
+ canCreateRelation: unsupportedReason === void 0,
182322
+ canDeleteRelation: unsupportedReason === void 0,
182323
+ ...unsupportedReason === void 0 ? {} : { unsupportedReason }
182149
182324
  };
182150
182325
  };
182151
182326
  var toCandidate = (association) => ({
182152
182327
  id: AssociationId.make(association._id),
182153
182328
  name: associationName(association),
182154
- sourceClass: association.classA,
182155
- targetClass: association.classB
182329
+ sourceClass: ObjectClassName.make(association.classA),
182330
+ targetClass: ObjectClassName.make(association.classB)
182156
182331
  });
182157
182332
  var matchesAssociationIdentifier = (association, identifier2) => {
182158
182333
  const normalized = identifier2.trim().toLowerCase();
@@ -182169,7 +182344,7 @@ var associationListFiltersFromParams = (params) => ({
182169
182344
  });
182170
182345
  var associationMatchesFilters = (association, filters) => (filters.includeSystem || !isSystemAssociation(association)) && (filters.sourceClass === void 0 || association.classA === String(filters.sourceClass)) && (filters.targetClass === void 0 || association.classB === String(filters.targetClass));
182171
182346
  var filterVisible = (associations, filters) => associations.filter(
182172
- (association) => associationMatchesFilters(association, filters) && !filters.writableOnly
182347
+ (association) => associationMatchesFilters(association, filters) && (!filters.writableOnly || isRelationWritableAssociation(association))
182173
182348
  );
182174
182349
  var listAssociationDocs = (client, params) => {
182175
182350
  const query = {};
@@ -182266,6 +182441,155 @@ var resolveAssociation = (client, identifier2, filters) => Effect_exports.gen(fu
182266
182441
  }
182267
182442
  return candidates[0];
182268
182443
  });
182444
+ var rejectSystemClass = (className, operation) => isSystemClassName(String(className)) ? Effect_exports.fail(new AssociationSystemClassUnsupportedError({ className, operation })) : Effect_exports.void;
182445
+ var systemClassInAssociation = (association) => {
182446
+ if (isSystemClassName(String(association.classA))) {
182447
+ return ObjectClassName.make(association.classA);
182448
+ }
182449
+ if (isSystemClassName(String(association.classB))) {
182450
+ return ObjectClassName.make(association.classB);
182451
+ }
182452
+ return void 0;
182453
+ };
182454
+ var ensureRelationMutationSupported = (association, operation) => {
182455
+ const systemClass = systemClassInAssociation(association);
182456
+ if (systemClass !== void 0) {
182457
+ return Effect_exports.fail(new AssociationSystemClassUnsupportedError({ className: systemClass, operation }));
182458
+ }
182459
+ if (associationAutomationOnly(association)) {
182460
+ return Effect_exports.fail(
182461
+ new RelationMutationUnsupportedError({
182462
+ associationId: AssociationId.make(association._id),
182463
+ reason: "association is automation-only"
182464
+ })
182465
+ );
182466
+ }
182467
+ return Effect_exports.void;
182468
+ };
182469
+ var exactAssociationQuery = (params) => ({
182470
+ classA: toClassRef(params.sourceClass),
182471
+ classB: toClassRef(params.targetClass),
182472
+ nameA: params.sourceRole,
182473
+ nameB: params.targetRole
182474
+ });
182475
+ var createdAssociationSummaryInput = (id, params) => ({
182476
+ _id: id,
182477
+ classA: toClassRef(params.sourceClass),
182478
+ classB: toClassRef(params.targetClass),
182479
+ nameA: params.sourceRole,
182480
+ nameB: params.targetRole,
182481
+ type: SDK_CARDINALITY[params.cardinality],
182482
+ automationOnly: params.automationOnly ?? false
182483
+ });
182484
+ var createAssociation = (params) => Effect_exports.gen(function* () {
182485
+ const client = yield* HulyClient;
182486
+ yield* rejectSystemClass(params.sourceClass, "create_association");
182487
+ yield* rejectSystemClass(params.targetClass, "create_association");
182488
+ const existing = yield* client.findOne(
182489
+ core.class.Association,
182490
+ hulyQuery(exactAssociationQuery(params))
182491
+ );
182492
+ if (existing !== void 0) {
182493
+ if (params.ifExists === "fail") {
182494
+ return yield* new AssociationConflictError({
182495
+ associationId: AssociationId.make(existing._id),
182496
+ reason: "ifExists=fail was requested"
182497
+ });
182498
+ }
182499
+ if (cardinality(existing.type) !== params.cardinality) {
182500
+ return yield* new AssociationConflictError({
182501
+ associationId: AssociationId.make(existing._id),
182502
+ reason: `existing cardinality is ${cardinality(existing.type)}, requested ${params.cardinality}`
182503
+ });
182504
+ }
182505
+ if (associationAutomationOnly(existing) !== (params.automationOnly ?? false)) {
182506
+ return yield* new AssociationConflictError({
182507
+ associationId: AssociationId.make(existing._id),
182508
+ reason: `existing automationOnly is ${associationAutomationOnly(existing)}, requested ${params.automationOnly ?? false}`
182509
+ });
182510
+ }
182511
+ return {
182512
+ association: toAssociationSummary(existing),
182513
+ created: false,
182514
+ existing: true
182515
+ };
182516
+ }
182517
+ const attributes = {
182518
+ classA: toClassRef(params.sourceClass),
182519
+ classB: toClassRef(params.targetClass),
182520
+ nameA: params.sourceRole,
182521
+ nameB: params.targetRole,
182522
+ type: SDK_CARDINALITY[params.cardinality],
182523
+ automationOnly: params.automationOnly ?? false
182524
+ };
182525
+ const associationId = yield* client.createDoc(
182526
+ core.class.Association,
182527
+ toRef(core.space.Model),
182528
+ attributes
182529
+ );
182530
+ return {
182531
+ association: toAssociationSummary(createdAssociationSummaryInput(associationId, params)),
182532
+ created: true,
182533
+ existing: false
182534
+ };
182535
+ });
182536
+ var ensureAssociationDeletionSupported = (association) => {
182537
+ const systemClass = systemClassInAssociation(association);
182538
+ return systemClass === void 0 ? Effect_exports.void : Effect_exports.fail(
182539
+ new AssociationSystemClassUnsupportedError({
182540
+ className: systemClass,
182541
+ operation: "delete_association"
182542
+ })
182543
+ );
182544
+ };
182545
+ var countAssociationRelations = (client, association) => Effect_exports.map(
182546
+ client.findAll(
182547
+ core.class.Relation,
182548
+ hulyQuery({
182549
+ association: toRef(association._id)
182550
+ }),
182551
+ { limit: 5 }
182552
+ ),
182553
+ (relations) => ({
182554
+ total: Math.max(relations.total, relations.length),
182555
+ sampleRelationIds: relations.map((relation) => RelationId.make(relation._id))
182556
+ })
182557
+ );
182558
+ var deleteAssociation = (params) => Effect_exports.gen(function* () {
182559
+ const client = yield* HulyClient;
182560
+ const association = yield* resolveAssociation(
182561
+ client,
182562
+ params.association,
182563
+ MUTATION_ASSOCIATION_FILTERS
182564
+ ).pipe(
182565
+ Effect_exports.catchTag("AssociationNotFoundError", () => Effect_exports.succeed(void 0))
182566
+ );
182567
+ if (association === void 0) {
182568
+ return {
182569
+ association: params.association,
182570
+ deleted: false,
182571
+ relationCount: 0,
182572
+ reason: "not_found"
182573
+ };
182574
+ }
182575
+ yield* ensureAssociationDeletionSupported(association);
182576
+ const relationUsage = yield* countAssociationRelations(client, association);
182577
+ if (relationUsage.total > 0) {
182578
+ return yield* new AssociationInUseError({
182579
+ associationId: AssociationId.make(association._id),
182580
+ relationCount: relationUsage.total,
182581
+ sampleRelationIds: relationUsage.sampleRelationIds
182582
+ });
182583
+ }
182584
+ yield* client.removeDoc(core.class.Association, association.space, association._id);
182585
+ return {
182586
+ association: params.association,
182587
+ associationId: AssociationId.make(association._id),
182588
+ deleted: true,
182589
+ relationCount: 0,
182590
+ reason: "deleted"
182591
+ };
182592
+ });
182269
182593
  var listAssociations = (params) => Effect_exports.gen(function* () {
182270
182594
  const client = yield* HulyClient;
182271
182595
  if (params.association !== void 0) {
@@ -182480,7 +182804,7 @@ var resolveRelationEndpointFromCache = (docsByClass, id, className) => {
182480
182804
  var relationToSummary = (association, relation, docsByClass) => ({
182481
182805
  relationId: RelationId.make(relation._id),
182482
182806
  associationId: AssociationId.make(association._id),
182483
- associationName: optionalNonEmpty(associationName(association)),
182807
+ associationName: associationName(association),
182484
182808
  source: resolveRelationEndpointFromCache(docsByClass, relation.docA, association.classA),
182485
182809
  target: resolveRelationEndpointFromCache(docsByClass, relation.docB, association.classB),
182486
182810
  createdOn: relation.createdOn,
@@ -182718,21 +183042,182 @@ var listRelations = (params) => Effect_exports.gen(function* () {
182718
183042
  total: summaries.length
182719
183043
  };
182720
183044
  });
183045
+ var resolveRelationWriteEndpoints = (client, association, params) => Effect_exports.gen(function* () {
183046
+ const direction = params.direction ?? DefaultRelationDirection;
183047
+ if (direction === "source-to-target") {
183048
+ const source2 = yield* resolveGenericObject(client, params.source, association.classA, "source");
183049
+ const target2 = yield* resolveGenericObject(client, params.target, association.classB, "target");
183050
+ return { docA: source2, docB: target2, source: source2, target: target2 };
183051
+ }
183052
+ if (direction === "target-to-source") {
183053
+ const source2 = yield* resolveGenericObject(client, params.source, association.classB, "source");
183054
+ const target2 = yield* resolveGenericObject(client, params.target, association.classA, "target");
183055
+ return { docA: target2, docB: source2, source: source2, target: target2 };
183056
+ }
183057
+ const source = yield* resolveGenericObject(client, params.source, void 0, "source");
183058
+ const target = yield* resolveGenericObject(client, params.target, void 0, "target");
183059
+ const matchesForward = String(source.class) === association.classA && String(target.class) === association.classB;
183060
+ const matchesReverse = String(source.class) === association.classB && String(target.class) === association.classA;
183061
+ if (matchesForward && matchesReverse) {
183062
+ return yield* new RelationDirectionAmbiguousError({
183063
+ associationId: AssociationId.make(association._id),
183064
+ reason: "both endpoints match both sides of the association"
183065
+ });
183066
+ }
183067
+ if (matchesForward) {
183068
+ return { docA: source, docB: target, source, target };
183069
+ }
183070
+ if (matchesReverse) {
183071
+ return { docA: target, docB: source, source, target };
183072
+ }
183073
+ yield* validateEitherEndpointClasses(association, source, target);
183074
+ return yield* new RelationEndpointClassMismatchError({
183075
+ field: "source",
183076
+ expectedClass: `${association.classA} or ${association.classB}`,
183077
+ actualClass: source.class
183078
+ });
183079
+ });
183080
+ var exactRelationQuery = (association, endpoints) => ({
183081
+ association: toRef(association._id),
183082
+ docA: toRef(endpoints.docA.id),
183083
+ docB: toRef(endpoints.docB.id)
183084
+ });
183085
+ var findExactRelations = (client, association, endpoints, limit) => Effect_exports.map(
183086
+ client.findAll(
183087
+ core.class.Relation,
183088
+ hulyQuery(exactRelationQuery(association, endpoints)),
183089
+ { limit }
183090
+ ),
183091
+ (relations) => [...relations]
183092
+ );
183093
+ var findCardinalityConflict = (client, association, endpoints) => Effect_exports.gen(function* () {
183094
+ if (association.type === "N:N") {
183095
+ return void 0;
183096
+ }
183097
+ const docBConflict = yield* client.findOne(
183098
+ core.class.Relation,
183099
+ hulyQuery({
183100
+ association: toRef(association._id),
183101
+ docB: toRef(endpoints.docB.id)
183102
+ })
183103
+ );
183104
+ if (docBConflict !== void 0) {
183105
+ return docBConflict;
183106
+ }
183107
+ if (association.type === "1:1") {
183108
+ return yield* client.findOne(
183109
+ core.class.Relation,
183110
+ hulyQuery({
183111
+ association: toRef(association._id),
183112
+ docA: toRef(endpoints.docA.id)
183113
+ })
183114
+ );
183115
+ }
183116
+ return void 0;
183117
+ });
183118
+ var enforceCardinality = (client, association, endpoints) => Effect_exports.gen(function* () {
183119
+ const conflict = yield* findCardinalityConflict(client, association, endpoints);
183120
+ if (conflict === void 0) {
183121
+ return;
183122
+ }
183123
+ const reason = association.type === "1:1" ? "one-to-one associations allow each endpoint to appear in only one relation" : "one-to-many associations allow each target-side endpoint to appear in only one relation";
183124
+ return yield* new RelationCardinalityViolationError({
183125
+ associationId: AssociationId.make(association._id),
183126
+ cardinality: cardinality(association.type),
183127
+ reason
183128
+ });
183129
+ });
182721
183130
  var createRelation = (params) => Effect_exports.gen(function* () {
182722
183131
  const client = yield* HulyClient;
182723
183132
  const association = yield* resolveAssociation(client, params.association, MUTATION_ASSOCIATION_FILTERS);
182724
- return yield* new RelationMutationUnsupportedError({
183133
+ yield* ensureRelationMutationSupported(association, "create_relation");
183134
+ const endpoints = yield* resolveRelationWriteEndpoints(client, association, params);
183135
+ const exact = yield* findExactRelations(client, association, endpoints, 1);
183136
+ const existing = exact.at(0);
183137
+ if (existing !== void 0) {
183138
+ if (params.ifExists === "fail") {
183139
+ return yield* new RelationCardinalityViolationError({
183140
+ associationId: AssociationId.make(association._id),
183141
+ cardinality: cardinality(association.type),
183142
+ reason: `relation '${existing._id}' already exists`
183143
+ });
183144
+ }
183145
+ return {
183146
+ relationId: RelationId.make(existing._id),
183147
+ associationId: AssociationId.make(association._id),
183148
+ source: endpoints.source,
183149
+ target: endpoints.target,
183150
+ created: false,
183151
+ existing: true
183152
+ };
183153
+ }
183154
+ yield* enforceCardinality(client, association, endpoints);
183155
+ const relationId = yield* client.createDoc(
183156
+ core.class.Relation,
183157
+ toRef(core.space.Workspace),
183158
+ {
183159
+ association: toRef(association._id),
183160
+ docA: toRef(endpoints.docA.id),
183161
+ docB: toRef(endpoints.docB.id)
183162
+ }
183163
+ );
183164
+ return {
183165
+ relationId: RelationId.make(relationId),
182725
183166
  associationId: AssociationId.make(association._id),
182726
- reason: WRITE_UNSUPPORTED_REASON
182727
- });
183167
+ source: endpoints.source,
183168
+ target: endpoints.target,
183169
+ created: true,
183170
+ existing: false
183171
+ };
182728
183172
  });
182729
183173
  var deleteRelation = (params) => Effect_exports.gen(function* () {
182730
183174
  const client = yield* HulyClient;
182731
- const association = "association" in params ? yield* resolveAssociation(client, params.association, MUTATION_ASSOCIATION_FILTERS) : void 0;
182732
- return yield* new RelationMutationUnsupportedError({
182733
- associationId: association === void 0 ? void 0 : AssociationId.make(association._id),
182734
- reason: WRITE_UNSUPPORTED_REASON
182735
- });
183175
+ if ("relation" in params) {
183176
+ const existing = yield* client.findOne(
183177
+ core.class.Relation,
183178
+ hulyQuery({ _id: toRef(params.relation) })
183179
+ );
183180
+ if (existing === void 0) {
183181
+ return {
183182
+ relationId: RelationId.make(params.relation),
183183
+ deleted: false,
183184
+ reason: "not_found"
183185
+ };
183186
+ }
183187
+ const association2 = yield* resolveAssociation(client, existing.association, MUTATION_ASSOCIATION_FILTERS);
183188
+ yield* ensureRelationMutationSupported(association2, "delete_relation");
183189
+ yield* client.removeDoc(core.class.Relation, existing.space, existing._id);
183190
+ return {
183191
+ relationId: RelationId.make(existing._id),
183192
+ associationId: AssociationId.make(association2._id),
183193
+ deleted: true,
183194
+ reason: "deleted"
183195
+ };
183196
+ }
183197
+ const association = yield* resolveAssociation(client, params.association, MUTATION_ASSOCIATION_FILTERS);
183198
+ yield* ensureRelationMutationSupported(association, "delete_relation");
183199
+ const endpoints = yield* resolveRelationWriteEndpoints(client, association, params);
183200
+ const matches = yield* findExactRelations(client, association, endpoints, 2);
183201
+ if (matches.length === 0) {
183202
+ return {
183203
+ associationId: AssociationId.make(association._id),
183204
+ deleted: false,
183205
+ reason: "not_found"
183206
+ };
183207
+ }
183208
+ if (matches.length > 1) {
183209
+ return yield* new RelationIdentifierAmbiguousError({
183210
+ identifier: `${params.association}/${endpoints.docA.id}/${endpoints.docB.id}`,
183211
+ relationIds: matches.map((relation) => RelationId.make(relation._id))
183212
+ });
183213
+ }
183214
+ yield* client.removeDoc(core.class.Relation, matches[0].space, matches[0]._id);
183215
+ return {
183216
+ relationId: RelationId.make(matches[0]._id),
183217
+ associationId: AssociationId.make(association._id),
183218
+ deleted: true,
183219
+ reason: "deleted"
183220
+ };
182736
183221
  });
182737
183222
 
182738
183223
  // src/mcp/tools/generic-associations.ts
@@ -182750,6 +183235,42 @@ var genericAssociationTools = [
182750
183235
  ListAssociationsResultSchema
182751
183236
  )
182752
183237
  },
183238
+ {
183239
+ name: "create_association",
183240
+ description: "Idempotently create one Huly association definition between two non-system classes. Use sourceClass/targetClass with sourceRole/targetRole and cardinality; returns an existing identical association by default.",
183241
+ category: CATEGORY11,
183242
+ inputSchema: createAssociationParamsJsonSchema,
183243
+ annotations: {
183244
+ readOnlyHint: false,
183245
+ destructiveHint: false,
183246
+ idempotentHint: true,
183247
+ openWorldHint: false
183248
+ },
183249
+ handler: createEncodedToolHandler(
183250
+ "create_association",
183251
+ parseCreateAssociationParams,
183252
+ createAssociation,
183253
+ CreateAssociationResultSchema
183254
+ )
183255
+ },
183256
+ {
183257
+ name: "delete_association",
183258
+ description: "Idempotently delete one Huly association definition only when no concrete relations reference it. If relations exist, delete_relation must clean them up first; deleting an already-missing association is a successful no-op.",
183259
+ category: CATEGORY11,
183260
+ inputSchema: deleteAssociationParamsJsonSchema,
183261
+ annotations: {
183262
+ readOnlyHint: false,
183263
+ destructiveHint: true,
183264
+ idempotentHint: true,
183265
+ openWorldHint: false
183266
+ },
183267
+ handler: createEncodedToolHandler(
183268
+ "delete_association",
183269
+ parseDeleteAssociationParams,
183270
+ deleteAssociation,
183271
+ DeleteAssociationResultSchema
183272
+ )
183273
+ },
182753
183274
  {
182754
183275
  name: "list_relations",
182755
183276
  description: "List concrete Huly relation instances under an association, optionally filtered by source and target documents. Requires at least one filter to avoid broad workspace scans.",
@@ -182764,7 +183285,7 @@ var genericAssociationTools = [
182764
183285
  },
182765
183286
  {
182766
183287
  name: "create_relation",
182767
- description: "Idempotently create one concrete relation between two resolved documents. Only succeeds for associations where list_associations reports canCreateRelation=true; otherwise it fails clearly. This build currently reports no generic associations as writable until a write allowlist is live-validated.",
183288
+ description: "Idempotently create one concrete relation between two resolved documents for a writable association. Enforces association endpoint classes, direction, duplicate handling, automation-only restrictions, and cardinality.",
182768
183289
  category: CATEGORY11,
182769
183290
  inputSchema: createRelationParamsJsonSchema,
182770
183291
  annotations: {
@@ -182782,9 +183303,15 @@ var genericAssociationTools = [
182782
183303
  },
182783
183304
  {
182784
183305
  name: "delete_relation",
182785
- description: "Idempotently delete one concrete relation by relation ID or by exact association/source/target triple. Only succeeds for associations where list_associations reports canDeleteRelation=true; otherwise it fails clearly. This build currently reports no generic associations as writable until a write allowlist is live-validated.",
183306
+ description: "Idempotently delete one concrete relation by relation ID or by exact association/source/target triple. Triple deletes use the same direction semantics as create_relation and fail if the selector is ambiguous.",
182786
183307
  category: CATEGORY11,
182787
183308
  inputSchema: deleteRelationParamsJsonSchema,
183309
+ annotations: {
183310
+ readOnlyHint: false,
183311
+ destructiveHint: true,
183312
+ idempotentHint: true,
183313
+ openWorldHint: false
183314
+ },
182788
183315
  handler: createEncodedToolHandler(
182789
183316
  "delete_relation",
182790
183317
  parseDeleteRelationParams,
@@ -188643,7 +189170,6 @@ var handleVersionTool = async () => {
188643
189170
  const latest = await fetchLatestNpmVersion();
188644
189171
  return createSuccessResponse({ current: VERSION, latest });
188645
189172
  };
188646
- var isObjectSchema = (schema) => "type" in schema && schema.type === "object";
188647
189173
  var McpServerError = class extends Schema_exports.TaggedError()(
188648
189174
  "McpServerError",
188649
189175
  {
@@ -188709,7 +189235,7 @@ var createMcpServer = (resolveClients, telemetry, registry2, createServer = () =
188709
189235
  return [{
188710
189236
  name: tool.name,
188711
189237
  description: tool.description,
188712
- inputSchema: tool.inputSchema,
189238
+ inputSchema: toClientCompatibleInputSchema(tool.inputSchema),
188713
189239
  outputSchema: defaultToolOutputSchema,
188714
189240
  annotations: resolveAnnotations(tool)
188715
189241
  }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firfi/huly-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "MCP server for Huly integration",
5
5
  "mcpName": "io.github.dearlordylord/huly-mcp",
6
6
  "type": "module",
@@ -107,10 +107,10 @@
107
107
  "check-format": "find src test -name '*.ts' -print0 | xargs -0 eslint --rule '@effect/dprint: error'",
108
108
  "check-all": "pnpm build && pnpm typecheck && pnpm verify-registry-metadata && pnpm lint && pnpm test",
109
109
  "circular": "madge --extensions ts --circular src",
110
- "local-release": "changeset version && pnpm sync-registry-metadata && pnpm build && pnpm verify-version && changeset publish",
110
+ "local-release": "pnpm dlx @changesets/cli@2.30.0 version && pnpm sync-registry-metadata && git add package.json CHANGELOG.md server.json .changeset && (git diff --cached --quiet || HUSKY=0 git commit -m \"RELEASING: Releasing 1 package(s)\") && PKG_VERSION=$(node -p \"require('./package.json').version\") && pnpm dlx esbuild@0.27.2 src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:ws \"--define:PKG_VERSION=\\\"$PKG_VERSION\\\"\" && pnpm verify-version && npm_config_ignore_scripts=true pnpm dlx @changesets/cli@2.30.0 publish",
111
111
  "verify-version": "node -e \"const v=require('./package.json').version; const d=require('fs').readFileSync('dist/index.cjs','utf8'); if(!d.includes('\\\"'+v+'\\\"'))throw new Error('dist version mismatch: expected '+v)\"",
112
- "verify-registry-metadata": "tsx scripts/sync-registry-metadata.ts --check",
113
- "sync-registry-metadata": "tsx scripts/sync-registry-metadata.ts",
114
- "update-readme": "tsx scripts/update-readme-tools.ts"
112
+ "verify-registry-metadata": "node scripts/sync-registry-metadata.mjs --check",
113
+ "sync-registry-metadata": "node scripts/sync-registry-metadata.mjs",
114
+ "update-readme": "node scripts/update-readme-tools.mjs"
115
115
  }
116
116
  }