@thorcollective/hearth-mcp 0.0.1 → 0.1.1

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 +30 -2
  2. package/dist/index.js +31 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,6 +86,31 @@ Add this to `.vscode/mcp.json` in your workspace:
86
86
 
87
87
  Set them in your client's config, for example `"env": { "HEARTH_OFFLINE": "1" }` next to `"args"`.
88
88
 
89
+ ### Locked-down setup (enterprise)
90
+
91
+ For teams that review every change before it reaches analysts:
92
+
93
+ - **Pin the version.** `npx -y @thorcollective/hearth-mcp` always fetches the latest release. Name an exact version instead, and nothing changes until you choose to upgrade.
94
+ - **Turn off the network.** `HEARTH_OFFLINE=1` stops the daily check, so the server only uses the hunt data that shipped with the version you pinned.
95
+ - **Or use your own mirror.** Set `HEARTH_DATA_URL` to an internal copy of the site's data files, which you update on your own schedule.
96
+ - **Verify the package.** After installing, run `npm audit signatures` to check that the package was built from this repository by GitHub Actions.
97
+
98
+ For example, in Claude Desktop:
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "hearth": {
104
+ "command": "npx",
105
+ "args": ["-y", "@thorcollective/hearth-mcp@0.1.1"],
106
+ "env": { "HEARTH_OFFLINE": "1" }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ [Releases](https://www.npmjs.com/package/@thorcollective/hearth-mcp?activeTab=versions) lists every version.
113
+
89
114
  ### Other clients
90
115
 
91
116
  Any MCP client that supports stdio servers can run the same command: `npx -y @thorcollective/hearth-mcp`.
@@ -124,7 +149,7 @@ The server reuses the site's own logic from `src/lib`, so search behaves the sam
124
149
 
125
150
  Maintainers only:
126
151
 
127
- 1. Bump `version` in `mcp/package.json` and `mcp/server.json`. A test fails if they differ.
152
+ 1. Bump `version` in `mcp/package.json`, `mcp/server.json` and the pinned example under **Locked-down setup** in this README. A test fails if any of them differ.
128
153
  2. Merge to `main`.
129
154
  3. Push a tag named `mcp-v<version>`, for example `mcp-v0.1.0`.
130
155
 
@@ -132,4 +157,7 @@ The [publish workflow](../.github/workflows/publish-mcp.yml) then:
132
157
 
133
158
  1. Waits for approval in the `npm-publish` environment.
134
159
  2. Publishes to npm with provenance, using trusted publishing (no npm token is stored).
135
- 3. Lists the release in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.THORCollective/hearth`.
160
+ 3. Waits for npm to serve the new version, which can take several minutes.
161
+ 4. Lists the release in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.THORCollective/hearth`.
162
+
163
+ If the run fails after the npm publish, use **Re-run failed jobs**. A version already on npm is never published twice, so the re-run picks up at the registry step.
package/dist/index.js CHANGED
@@ -567,6 +567,11 @@ async function refresh(opts, meta) {
567
567
  import { McpServer } from "@modelcontextprotocol/server";
568
568
  import * as z2 from "zod/v4";
569
569
 
570
+ // ../src/lib/coverage.ts
571
+ function gapSubmitUrl(techniqueId) {
572
+ return `submit.html?technique=${encodeURIComponent(techniqueId)}`;
573
+ }
574
+
570
575
  // ../src/lib/hunt-search.ts
571
576
  var DEFAULT_LIMIT = 20;
572
577
  var MAX_LIMIT = 100;
@@ -678,9 +683,13 @@ function searchHunts(hunts, q, index = EMPTY_INDEX) {
678
683
  }
679
684
 
680
685
  // src/server.ts
681
- var VERSION = true ? "0.0.1" : "dev";
686
+ var VERSION = true ? "0.1.1" : "dev";
682
687
  var REPO_URL = "https://github.com/THORCollective/HEARTH/blob/main/";
683
688
  var MAX_GAPS = 25;
689
+ var SITE_URL = "https://hearth.thorcollective.com/";
690
+ var SUBMIT_URL = SITE_URL + "submit.html";
691
+ var SUBMIT_HUNT = `No HEARTH hunt covers this yet. Anyone can submit one: ${SUBMIT_URL}`;
692
+ var SubmitHunt = z2.string().optional().describe("Present when HEARTH has no hunt for part of the request: where to submit one");
684
693
  var DataSourceList = z2.array(z2.enum(DATA_SOURCE_IDS)).min(1).max(DATA_SOURCE_IDS.length).describe(
685
694
  "Data source categories you have: endpoint (EDR/Sysmon), windows (Windows event logs), network (firewall/flow/DNS/IDS), cloud (cloud & M365/Workspace audit), identity (AD/IdP/auth), email, web_proxy, application (SaaS/CI/web server), macos, appliance (network appliance & hypervisor logs)"
686
695
  );
@@ -835,7 +844,8 @@ function createServer(source) {
835
844
  }),
836
845
  outputSchema: z2.object({
837
846
  total: z2.number().int(),
838
- hunts: z2.array(HuntSummary)
847
+ hunts: z2.array(HuntSummary),
848
+ submit_hunt: SubmitHunt
839
849
  }),
840
850
  annotations: { readOnlyHint: true, openWorldHint: false }
841
851
  },
@@ -853,7 +863,11 @@ function createServer(source) {
853
863
  },
854
864
  data.index
855
865
  );
856
- return result({ total, hunts: hunts.map((h) => summary(h, data)) });
866
+ return result({
867
+ total,
868
+ hunts: hunts.map((h) => summary(h, data)),
869
+ submit_hunt: total === 0 ? SUBMIT_HUNT : void 0
870
+ });
857
871
  }
858
872
  );
859
873
  server.registerTool(
@@ -895,7 +909,8 @@ function createServer(source) {
895
909
  )
896
910
  })
897
911
  ),
898
- gaps: z2.array(z2.object({ technique: z2.string(), tactic: z2.string() })).describe(`Actor techniques no HEARTH hunt covers (first ${MAX_GAPS})`)
912
+ gaps: z2.array(z2.object({ technique: z2.string(), tactic: z2.string(), submit_url: z2.string() })).describe(`Actor techniques no HEARTH hunt covers (first ${MAX_GAPS})`),
913
+ submit_hunt: SubmitHunt
899
914
  }),
900
915
  annotations: { readOnlyHint: true, openWorldHint: false }
901
916
  },
@@ -920,6 +935,7 @@ function createServer(source) {
920
935
  })).sort(
921
936
  (a, b) => Number(b.reasons.includes("MENTION")) - Number(a.reasons.includes("MENTION")) || Number(b.missing?.length === 0) - Number(a.missing?.length === 0) || b.relevance - a.relevance || idNumber2(b.hunt.id) - idNumber2(a.hunt.id)
922
937
  );
938
+ const gaps = computeGap(actor.id, allMatches, data.actors);
923
939
  const url = actor.external_references?.find((r) => r.source === "mitre-attack")?.url;
924
940
  return result({
925
941
  actor: { id: groupId(actor.id), name: actor.label, aliases: actor.aliases, url },
@@ -939,7 +955,12 @@ function createServer(source) {
939
955
  shared_techniques: m.sharedTechniques,
940
956
  relevance: m.relevance
941
957
  })),
942
- gaps: computeGap(actor.id, allMatches, data.actors).slice(0, MAX_GAPS).map((g) => ({ technique: g.id, tactic: g.tactic }))
958
+ gaps: gaps.slice(0, MAX_GAPS).map((g) => ({
959
+ technique: g.id,
960
+ tactic: g.tactic,
961
+ submit_url: SITE_URL + gapSubmitUrl(g.id)
962
+ })),
963
+ submit_hunt: gaps.length > 0 || matches.length === 0 ? SUBMIT_HUNT : void 0
943
964
  });
944
965
  }
945
966
  );
@@ -964,7 +985,8 @@ function createServer(source) {
964
985
  have_all_sources: z2.boolean(),
965
986
  missing_sources: z2.array(z2.enum(DATA_SOURCE_IDS))
966
987
  })
967
- )
988
+ ),
989
+ submit_hunt: SubmitHunt
968
990
  }),
969
991
  annotations: { readOnlyHint: true, openWorldHint: false }
970
992
  },
@@ -993,7 +1015,8 @@ function createServer(source) {
993
1015
  total: rows.length,
994
1016
  full_matches: rows.filter((r) => r.have_all_sources).length,
995
1017
  by_category: byCategory,
996
- hunts: rows.slice(0, args.limit ?? DEFAULT_LIMIT)
1018
+ hunts: rows.slice(0, args.limit ?? DEFAULT_LIMIT),
1019
+ submit_hunt: rows.length === 0 ? SUBMIT_HUNT : void 0
997
1020
  });
998
1021
  }
999
1022
  );
@@ -1009,7 +1032,7 @@ async function main() {
1009
1032
  ...config,
1010
1033
  bundledDir: DATA_DIR,
1011
1034
  cacheDir: process.env.HEARTH_CACHE_DIR || defaultCacheDir(),
1012
- snapshotAt: true ? "2026-11-09T21:50:25.103Z" : void 0,
1035
+ snapshotAt: true ? "2026-11-10T16:49:28.515Z" : void 0,
1013
1036
  userAgent: `hearth-mcp/${VERSION}`,
1014
1037
  log
1015
1038
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thorcollective/hearth-mcp",
3
3
  "mcpName": "io.github.THORCollective/hearth",
4
- "version": "0.0.1",
4
+ "version": "0.1.1",
5
5
  "description": "Read-only MCP server for HEARTH, the community threat hunting hypothesis library",
6
6
  "keywords": [
7
7
  "mcp",