@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.
- package/README.md +30 -2
- package/dist/index.js +31 -8
- 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
|
|
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.
|
|
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.
|
|
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({
|
|
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:
|
|
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-
|
|
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