@firfi/huly-mcp 0.1.37 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,6 +167,9 @@ MCP_TRANSPORT=http MCP_HTTP_PORT=8080 MCP_HTTP_HOST=0.0.0.0 npx -y @firfi/huly-m
167
167
  | `create_issue_from_template` | Create a new issue from a template. Applies template defaults, allowing overrides for specific fields. Returns the created issue identifier. |
168
168
  | `update_issue_template` | Update fields on an existing Huly issue template. Only provided fields are modified. |
169
169
  | `delete_issue_template` | Permanently delete a Huly issue template. This action cannot be undone. |
170
+ | `add_issue_relation` | Add a relation between two issues. Relation types: 'blocks' (source blocks target — pushes into target's blockedBy), 'is-blocked-by' (source is blocked by target — pushes into source's blockedBy), 'relates-to' (bidirectional link — updates both sides). targetIssue accepts cross-project identifiers like 'OTHER-42'. No-op if the relation already exists. |
171
+ | `remove_issue_relation` | Remove a relation between two issues. Mirrors add_issue_relation: 'blocks' pulls from target's blockedBy, 'is-blocked-by' pulls from source's blockedBy, 'relates-to' pulls from both sides. No-op if the relation doesn't exist. |
172
+ | `list_issue_relations` | List all relations of an issue. Returns blockedBy (issues blocking this one) and relations (bidirectional links) with resolved identifiers. Does NOT return issues that this issue blocks — use list_issue_relations on the target issue to see that. |
170
173
 
171
174
  ### Comments
172
175
 
package/dist/index.cjs CHANGED
@@ -173663,7 +173663,7 @@ var PostHog = class extends PostHogBackendClient {
173663
173663
  };
173664
173664
 
173665
173665
  // src/version.ts
173666
- var VERSION = true ? "0.1.35" : "0.0.0-dev";
173666
+ var VERSION = true ? "0.1.37" : "0.0.0-dev";
173667
173667
 
173668
173668
  // src/telemetry/posthog.ts
173669
173669
  var POSTHOG_API_KEY = "phc_TGfFqCGdnF0p68wuFzd5WSw1IsBvOJW0YgoMJDyZPjm";
@@ -177114,6 +177114,51 @@ var FulltextSearchParamsSchema = Schema_exports.Struct({
177114
177114
  var fulltextSearchParamsJsonSchema = JSONSchema_exports.make(FulltextSearchParamsSchema);
177115
177115
  var parseFulltextSearchParams = Schema_exports.decodeUnknown(FulltextSearchParamsSchema);
177116
177116
 
177117
+ // src/domain/schemas/relations.ts
177118
+ var RelationTypeValues = ["blocks", "is-blocked-by", "relates-to"];
177119
+ var RelationTypeSchema = Schema_exports.Literal(...RelationTypeValues).annotations({
177120
+ title: "RelationType",
177121
+ description: "Type of issue relation: 'blocks' (source blocks target), 'is-blocked-by' (source is blocked by target), 'relates-to' (bidirectional link)",
177122
+ jsonSchema: { type: "string", enum: [...RelationTypeValues] }
177123
+ });
177124
+ var issueRelationFields = {
177125
+ project: ProjectIdentifier.annotations({
177126
+ description: "Project identifier of the source issue (e.g., 'HULY')"
177127
+ }),
177128
+ issueIdentifier: IssueIdentifier.annotations({
177129
+ description: "Source issue identifier (e.g., 'HULY-123')"
177130
+ }),
177131
+ targetIssue: IssueIdentifier.annotations({
177132
+ description: "Target issue identifier \u2014 same project: '42' or 'PROJ-42'; cross-project: 'OTHER-42'"
177133
+ }),
177134
+ relationType: RelationTypeSchema
177135
+ };
177136
+ var AddIssueRelationParamsSchema = Schema_exports.Struct(issueRelationFields).annotations({
177137
+ title: "AddIssueRelationParams",
177138
+ description: "Parameters for adding a relation between two issues"
177139
+ });
177140
+ var RemoveIssueRelationParamsSchema = Schema_exports.Struct(issueRelationFields).annotations({
177141
+ title: "RemoveIssueRelationParams",
177142
+ description: "Parameters for removing a relation between two issues"
177143
+ });
177144
+ var ListIssueRelationsParamsSchema = Schema_exports.Struct({
177145
+ project: ProjectIdentifier.annotations({
177146
+ description: "Project identifier (e.g., 'HULY')"
177147
+ }),
177148
+ issueIdentifier: IssueIdentifier.annotations({
177149
+ description: "Issue identifier (e.g., 'HULY-123')"
177150
+ })
177151
+ }).annotations({
177152
+ title: "ListIssueRelationsParams",
177153
+ description: "Parameters for listing all relations of an issue"
177154
+ });
177155
+ var addIssueRelationParamsJsonSchema = JSONSchema_exports.make(AddIssueRelationParamsSchema);
177156
+ var removeIssueRelationParamsJsonSchema = JSONSchema_exports.make(RemoveIssueRelationParamsSchema);
177157
+ var listIssueRelationsParamsJsonSchema = JSONSchema_exports.make(ListIssueRelationsParamsSchema);
177158
+ var parseAddIssueRelationParams = Schema_exports.decodeUnknown(AddIssueRelationParamsSchema);
177159
+ var parseRemoveIssueRelationParams = Schema_exports.decodeUnknown(RemoveIssueRelationParamsSchema);
177160
+ var parseListIssueRelationsParams = Schema_exports.decodeUnknown(ListIssueRelationsParamsSchema);
177161
+
177117
177162
  // src/domain/schemas/notifications.ts
177118
177163
  var ListNotificationsParamsSchema = Schema_exports.Struct({
177119
177164
  limit: Schema_exports.optional(
@@ -179515,6 +179560,165 @@ var deleteIssueTemplate = (params) => Effect_exports.gen(function* () {
179515
179560
  return { id: IssueTemplateId.make(template._id), deleted: true };
179516
179561
  });
179517
179562
 
179563
+ // src/huly/operations/relations.ts
179564
+ var resolveTargetIssue = (client, sourceProject, targetIssueStr) => Effect_exports.gen(function* () {
179565
+ const { fullIdentifier } = parseIssueIdentifier(targetIssueStr, sourceProject.identifier);
179566
+ const match16 = fullIdentifier.match(/^([A-Z]+)-\d+$/i);
179567
+ const prefix = match16 ? match16[1].toUpperCase() : null;
179568
+ if (prefix !== null && prefix !== sourceProject.identifier.toUpperCase()) {
179569
+ const { client: c, project: targetProject } = yield* findProject(prefix);
179570
+ const issue3 = yield* findIssueInProject(c, targetProject, targetIssueStr);
179571
+ return { issue: issue3, project: targetProject };
179572
+ }
179573
+ const issue2 = yield* findIssueInProject(client, sourceProject, targetIssueStr);
179574
+ return { issue: issue2, project: sourceProject };
179575
+ });
179576
+ var makeRelatedDoc = (issue2) => ({
179577
+ _id: toRef(issue2._id),
179578
+ _class: toRef(tracker.class.Issue)
179579
+ });
179580
+ var hasRelation = (arr, targetId) => arr?.some((r) => r._id === toRef(targetId)) ?? false;
179581
+ var addIssueRelation = (params) => Effect_exports.gen(function* () {
179582
+ const { client, issue: source, project: project3 } = yield* findProjectAndIssue({
179583
+ project: params.project,
179584
+ identifier: params.issueIdentifier
179585
+ });
179586
+ const { issue: target, project: targetProject } = yield* resolveTargetIssue(
179587
+ client,
179588
+ project3,
179589
+ params.targetIssue
179590
+ );
179591
+ const result = { sourceIssue: source.identifier, targetIssue: target.identifier, relationType: params.relationType };
179592
+ switch (params.relationType) {
179593
+ case "blocks": {
179594
+ if (hasRelation(target.blockedBy, source._id)) {
179595
+ return { ...result, added: false };
179596
+ }
179597
+ yield* client.updateDoc(
179598
+ tracker.class.Issue,
179599
+ targetProject._id,
179600
+ target._id,
179601
+ { $push: { blockedBy: makeRelatedDoc(source) } }
179602
+ );
179603
+ return { ...result, added: true };
179604
+ }
179605
+ case "is-blocked-by": {
179606
+ if (hasRelation(source.blockedBy, target._id)) {
179607
+ return { ...result, added: false };
179608
+ }
179609
+ yield* client.updateDoc(
179610
+ tracker.class.Issue,
179611
+ project3._id,
179612
+ source._id,
179613
+ { $push: { blockedBy: makeRelatedDoc(target) } }
179614
+ );
179615
+ return { ...result, added: true };
179616
+ }
179617
+ case "relates-to": {
179618
+ if (hasRelation(source.relations, target._id)) {
179619
+ return { ...result, added: false };
179620
+ }
179621
+ yield* client.updateDoc(
179622
+ tracker.class.Issue,
179623
+ project3._id,
179624
+ source._id,
179625
+ { $push: { relations: makeRelatedDoc(target) } }
179626
+ );
179627
+ yield* client.updateDoc(
179628
+ tracker.class.Issue,
179629
+ targetProject._id,
179630
+ target._id,
179631
+ { $push: { relations: makeRelatedDoc(source) } }
179632
+ );
179633
+ return { ...result, added: true };
179634
+ }
179635
+ }
179636
+ });
179637
+ var removeIssueRelation = (params) => Effect_exports.gen(function* () {
179638
+ const { client, issue: source, project: project3 } = yield* findProjectAndIssue({
179639
+ project: params.project,
179640
+ identifier: params.issueIdentifier
179641
+ });
179642
+ const { issue: target, project: targetProject } = yield* resolveTargetIssue(
179643
+ client,
179644
+ project3,
179645
+ params.targetIssue
179646
+ );
179647
+ const result = { sourceIssue: source.identifier, targetIssue: target.identifier, relationType: params.relationType };
179648
+ switch (params.relationType) {
179649
+ case "blocks": {
179650
+ if (!hasRelation(target.blockedBy, source._id)) {
179651
+ return { ...result, removed: false };
179652
+ }
179653
+ yield* client.updateDoc(
179654
+ tracker.class.Issue,
179655
+ targetProject._id,
179656
+ target._id,
179657
+ { $pull: { blockedBy: { _id: toRef(source._id) } } }
179658
+ );
179659
+ return { ...result, removed: true };
179660
+ }
179661
+ case "is-blocked-by": {
179662
+ if (!hasRelation(source.blockedBy, target._id)) {
179663
+ return { ...result, removed: false };
179664
+ }
179665
+ yield* client.updateDoc(
179666
+ tracker.class.Issue,
179667
+ project3._id,
179668
+ source._id,
179669
+ { $pull: { blockedBy: { _id: toRef(target._id) } } }
179670
+ );
179671
+ return { ...result, removed: true };
179672
+ }
179673
+ case "relates-to": {
179674
+ if (!hasRelation(source.relations, target._id)) {
179675
+ return { ...result, removed: false };
179676
+ }
179677
+ yield* client.updateDoc(
179678
+ tracker.class.Issue,
179679
+ project3._id,
179680
+ source._id,
179681
+ { $pull: { relations: { _id: toRef(target._id) } } }
179682
+ );
179683
+ yield* client.updateDoc(
179684
+ tracker.class.Issue,
179685
+ targetProject._id,
179686
+ target._id,
179687
+ { $pull: { relations: { _id: toRef(source._id) } } }
179688
+ );
179689
+ return { ...result, removed: true };
179690
+ }
179691
+ }
179692
+ });
179693
+ var listIssueRelations = (params) => Effect_exports.gen(function* () {
179694
+ const { client, issue: issue2 } = yield* findProjectAndIssue({
179695
+ project: params.project,
179696
+ identifier: params.issueIdentifier
179697
+ });
179698
+ const blockedByRefs = issue2.blockedBy ?? [];
179699
+ const relationsRefs = issue2.relations ?? [];
179700
+ const allIds = [...blockedByRefs, ...relationsRefs].map((r) => r._id);
179701
+ if (allIds.length === 0) {
179702
+ return { blockedBy: [], relations: [] };
179703
+ }
179704
+ const toIssueRef = toRef;
179705
+ const issueIds = allIds.map(toIssueRef);
179706
+ const issues = yield* client.findAll(
179707
+ tracker.class.Issue,
179708
+ { _id: { $in: issueIds } }
179709
+ );
179710
+ const idToIdentifier = new Map(issues.map((i) => [String(i._id), i.identifier]));
179711
+ const toEntry = (r) => ({
179712
+ identifier: idToIdentifier.get(String(r._id)) ?? String(r._id),
179713
+ _id: String(r._id),
179714
+ _class: String(r._class)
179715
+ });
179716
+ return {
179717
+ blockedBy: blockedByRefs.map(toEntry),
179718
+ relations: relationsRefs.map(toEntry)
179719
+ };
179720
+ });
179721
+
179518
179722
  // src/mcp/tools/issues.ts
179519
179723
  var CATEGORY9 = "issues";
179520
179724
  var issueTools = [
@@ -179715,6 +179919,39 @@ var issueTools = [
179715
179919
  parseDeleteIssueTemplateParams,
179716
179920
  deleteIssueTemplate
179717
179921
  )
179922
+ },
179923
+ {
179924
+ name: "add_issue_relation",
179925
+ description: "Add a relation between two issues. Relation types: 'blocks' (source blocks target \u2014 pushes into target's blockedBy), 'is-blocked-by' (source is blocked by target \u2014 pushes into source's blockedBy), 'relates-to' (bidirectional link \u2014 updates both sides). targetIssue accepts cross-project identifiers like 'OTHER-42'. No-op if the relation already exists.",
179926
+ category: CATEGORY9,
179927
+ inputSchema: addIssueRelationParamsJsonSchema,
179928
+ handler: createToolHandler(
179929
+ "add_issue_relation",
179930
+ parseAddIssueRelationParams,
179931
+ addIssueRelation
179932
+ )
179933
+ },
179934
+ {
179935
+ name: "remove_issue_relation",
179936
+ description: "Remove a relation between two issues. Mirrors add_issue_relation: 'blocks' pulls from target's blockedBy, 'is-blocked-by' pulls from source's blockedBy, 'relates-to' pulls from both sides. No-op if the relation doesn't exist.",
179937
+ category: CATEGORY9,
179938
+ inputSchema: removeIssueRelationParamsJsonSchema,
179939
+ handler: createToolHandler(
179940
+ "remove_issue_relation",
179941
+ parseRemoveIssueRelationParams,
179942
+ removeIssueRelation
179943
+ )
179944
+ },
179945
+ {
179946
+ name: "list_issue_relations",
179947
+ description: "List all relations of an issue. Returns blockedBy (issues blocking this one) and relations (bidirectional links) with resolved identifiers. Does NOT return issues that this issue blocks \u2014 use list_issue_relations on the target issue to see that.",
179948
+ category: CATEGORY9,
179949
+ inputSchema: listIssueRelationsParamsJsonSchema,
179950
+ handler: createToolHandler(
179951
+ "list_issue_relations",
179952
+ parseListIssueRelationsParams,
179953
+ listIssueRelations
179954
+ )
179718
179955
  }
179719
179956
  ];
179720
179957