@mambalabsdev/mcp-github-organization-signal-scanner 1.0.0 → 1.1.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.
- package/README.md +142 -0
- package/build/index.js +138 -49
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# GitHub Organization Signal Scanner MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/servers/mambabuilt/mcp-github-organization-signal-scanner) [](https://glama.ai/mcp/servers/mambalabsdev/mcp-github-organization-signal-scanner) [](https://registry.modelcontextprotocol.io/v0/servers?search=com.mambabuilt/mcp-github-organization-signal-scanner&limit=1) [](https://www.npmjs.com/package/@mambalabsdev/mcp-github-organization-signal-scanner) [](https://www.npmjs.com/package/@mambalabsdev/mcp-github-organization-signal-scanner) [](https://github.com/mambalabsdev/mcp-github-organization-signal-scanner/blob/main/LICENSE) [](https://mcpservers.org/servers/mambalabsdev/mcp-github-organization-signal-scanner)
|
|
4
|
+
|
|
5
|
+
An MCP server that resolves a company domain to its GitHub organization with repo, language and activity signals. It wraps the Mamba Labs GitHub Organization Signal Scanner actor on Apify and returns a Clay-ready flat JSON row to any MCP client.
|
|
6
|
+
|
|
7
|
+
## What's Inside
|
|
8
|
+
|
|
9
|
+
- [What it does](#what-it-does)
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [Prerequisites](#prerequisites)
|
|
12
|
+
- [Example prompts](#example-prompts)
|
|
13
|
+
- [Inputs](#inputs)
|
|
14
|
+
- [Output](#output)
|
|
15
|
+
- [Example output](#example-output)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Full actor documentation](#full-actor-documentation)
|
|
18
|
+
- [Mamba Labs GTM Suite](#mamba-labs-gtm-suite)
|
|
19
|
+
- [License](#license)
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
Give it a company domain and it finds that company's GitHub organization and returns repository count, followers, creation date, top languages, total stars, the most recent push, how many repositories are actively worked on, and whether the company ships a public SDK. One flat row per company, read through GitHub's own documented REST API.
|
|
24
|
+
|
|
25
|
+
Forks and archived repositories are excluded from the language and star derivations, and the exclusion is counted on the row. A rate limited request reports `not_extractable` and never a zero, because a zero is a number a buyer would filter on. A login that resolves to a personal user account rather than an organization is reported as `identity_mismatch`. It reads organization and repository metadata only: it does not clone repositories, read source, scan for secrets or assess code quality.
|
|
26
|
+
|
|
27
|
+
All of the scanning runs on Apify. This package is a thin client that calls the actor and hands back the result unchanged.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
You need Node.js 18 or newer and an Apify account with an API token.
|
|
32
|
+
|
|
33
|
+
Add this to your Claude Desktop config:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"mamba-github-organization-signal-scanner": {
|
|
39
|
+
"command": "npx",
|
|
40
|
+
"args": ["-y", "@mambalabsdev/mcp-github-organization-signal-scanner"],
|
|
41
|
+
"env": {
|
|
42
|
+
"APIFY_TOKEN": "your-apify-token"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Get your token at https://console.apify.com/account/integrations, paste it in, and restart Claude Desktop. The `scan_github_organization_signals` tool will be available.
|
|
50
|
+
|
|
51
|
+
## Prerequisites
|
|
52
|
+
|
|
53
|
+
- Node.js 18 or newer
|
|
54
|
+
- An Apify account with an API token
|
|
55
|
+
- Optional: your own GitHub personal access token, free to create at github.com/settings/tokens with no scopes at all for public data, if you want to scan more than a handful of companies
|
|
56
|
+
|
|
57
|
+
## Example prompts
|
|
58
|
+
|
|
59
|
+
- "Find the GitHub organization for stripe.com and summarize its activity."
|
|
60
|
+
- "Scan the GitHub org `vercel` and tell me its top languages and total stars."
|
|
61
|
+
- "Does figma.com ship a public SDK? Check its GitHub organization."
|
|
62
|
+
- "Scan notion.so's GitHub org, counting a repo as active if it was pushed in the last 30 days."
|
|
63
|
+
|
|
64
|
+
## Inputs
|
|
65
|
+
|
|
66
|
+
- `company_domain` (optional): bare company domain, for example `stripe.com`. This is the lookup key and the join key for every other actor in the fleet.
|
|
67
|
+
- `company_name` (optional but strongly recommended): what the identity gate checks a discovered record against, so supplying it is the cheapest way to reduce wrong matches.
|
|
68
|
+
- `github_org` (optional): if you already know the organization login, for example `stripe`, put it here and the tool skips discovery entirely and goes straight to the API, which is faster and spends fewer rate limited requests.
|
|
69
|
+
- `includeRepoDetail` (optional): when true (the default) the organization repositories are read and the language, star, activity and SDK signals are derived from them. Set false to return the organization record only, which is one request instead of several and is friendlier to the unauthenticated rate limit.
|
|
70
|
+
- `includeContributorEstimate` (optional): when true one extra request reads the contributor count of the organization's most starred repository, as a floor on the size of its public engineering surface. False by default, because one extra request per company is real money against a 60 per hour unauthenticated budget.
|
|
71
|
+
- `activeWindowDays` (optional): how recently a repository must have been pushed to count as active. One of `30`, `90`, `180` or `365`, with 90 the default. This changes what active means on the row.
|
|
72
|
+
- `repoPageBudget` (optional): how many pages of 100 repositories to read for a large organization. One of `1`, `2`, `3` or `5`. This is a cost and completeness dial, not a change of answer: the row always reports how many repositories were sampled and whether the sample is complete.
|
|
73
|
+
- `githubToken` (optional): your own GitHub personal access token, free to create with no scopes at all for public data. Without it the tool runs at GitHub's 60 requests per hour, which is enough for a handful of companies and not enough for a list. With it the limit is 5,000 per hour.
|
|
74
|
+
- `skipCache` (optional): when false (the default) a successful lookup is cached for seven days and reused. Set true to force a fresh fetch.
|
|
75
|
+
|
|
76
|
+
## Output
|
|
77
|
+
|
|
78
|
+
The tool returns the actor's flat JSON row for the company, with 32 snake_case fields and no nested objects. `github_discovery` says how the organization was found, `github_rejected_candidate` records a candidate the identity gate turned away, `repos_sampled` and `repos_complete` say how much of the organization was read, and `repos_excluded` counts the forks and archived repositories left out. See the Apify Store page for the full output schema.
|
|
79
|
+
|
|
80
|
+
## Example output
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"degraded": false,
|
|
85
|
+
"degradation_reason": null,
|
|
86
|
+
"company_domain": "stripe.com",
|
|
87
|
+
"company_name": "Stripe",
|
|
88
|
+
"github_org": "stripe",
|
|
89
|
+
"github_url": "https://github.com/stripe",
|
|
90
|
+
"github_discovery": "homepage_link",
|
|
91
|
+
"github_rejected_candidate": null,
|
|
92
|
+
"github_account_type": "Organization",
|
|
93
|
+
"public_repos": 98,
|
|
94
|
+
"followers": 3469,
|
|
95
|
+
"org_created_at": "2011-06-17T15:42:37Z",
|
|
96
|
+
"org_location": "San Francisco, CA",
|
|
97
|
+
"top_languages": "TypeScript, Go, Ruby, JavaScript, HTML",
|
|
98
|
+
"total_stars": 42821,
|
|
99
|
+
"most_starred_repo": "stripe-node",
|
|
100
|
+
"most_starred_repo_stars": 4485,
|
|
101
|
+
"most_recent_push_at": "2026-08-22T18:33:15.000Z",
|
|
102
|
+
"active_repos_90d": 56,
|
|
103
|
+
"has_public_sdk": true,
|
|
104
|
+
"repos_sampled": 98,
|
|
105
|
+
"repos_complete": true,
|
|
106
|
+
"repos_excluded": "forks=6, archived=5",
|
|
107
|
+
"rate_limit_remaining": 57,
|
|
108
|
+
"coverage": 1,
|
|
109
|
+
"fetch_status": "ok",
|
|
110
|
+
"run_date": "2026-08-22T19:23:50.177Z"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Features
|
|
115
|
+
|
|
116
|
+
- Resolves a GitHub organization starting from a company domain
|
|
117
|
+
- Repository count, stars, followers and top languages
|
|
118
|
+
- Recent push activity and repositories active in a window you choose
|
|
119
|
+
- Public SDK detection, a direct developer tooling signal
|
|
120
|
+
- Runs keyless, with your own GitHub token available for scale
|
|
121
|
+
- Rejected candidates recorded in `github_rejected_candidate`
|
|
122
|
+
- 32 flat snake_case fields, one row per company
|
|
123
|
+
|
|
124
|
+
## Full actor documentation
|
|
125
|
+
|
|
126
|
+
This server is a thin client and holds no scanning logic. For the complete input and output reference, pricing, and run history, see the Apify Store page:
|
|
127
|
+
|
|
128
|
+
https://apify.com/mambalabs/github-organization-signal-scanner
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Mamba Labs GTM Suite
|
|
133
|
+
|
|
134
|
+
This server is one of the Mamba Labs GTM Suite MCP servers. Every actor in the suite takes a domain or a company and returns one flat row, so they stack in the same Clay table without reshaping anything. The actor behind this server is the GitHub Organization Signal Scanner, immutable Apify actor ID `ahF8RRpv5mzPao5CN`.
|
|
135
|
+
|
|
136
|
+
> Built by [Mamba Labs](https://github.com/mambalabsdev) | [npm](https://www.npmjs.com/org/mambalabsdev) | [Apify Store](https://apify.com/mambalabs)
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
141
|
+
|
|
142
|
+
Built by Mamba Labs. https://apify.com/mambalabs
|
package/build/index.js
CHANGED
|
@@ -9,7 +9,6 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
10
10
|
// Distinctive UA so Apify run meta.userAgent marks MCP-originated runs.
|
|
11
11
|
const USER_AGENT = `mambalabs-mcp ${pkg.name}@${pkg.version}`;
|
|
12
|
-
const APIFY_TOKEN = process.env.APIFY_TOKEN;
|
|
13
12
|
// Drop undefined values so optional inputs are not sent to the actor at all.
|
|
14
13
|
function compact(obj) {
|
|
15
14
|
const out = {};
|
|
@@ -26,36 +25,46 @@ function compact(obj) {
|
|
|
26
25
|
function boolToString(v) {
|
|
27
26
|
return v === undefined ? undefined : v ? "true" : "false";
|
|
28
27
|
}
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// How long the actor run itself is allowed to take, in seconds. One value for
|
|
29
|
+
// every Mamba Labs wrapper, set 2026-10-05: start and poll exists so a long run
|
|
30
|
+
// survives, and a shorter limit would end the long runs it was built for. Past
|
|
31
|
+
// this limit the run ends TIMED-OUT and the caller is told so, with the run id.
|
|
32
|
+
const ACTOR_RUN_TIMEOUT_SECS = 1800;
|
|
33
|
+
// memory=512 matches the actor's declared defaultRunOptions.memoryMbytes and
|
|
34
|
+
// is pinned so the run starts at the size the actor asks for: `apify-actor-start`
|
|
35
|
+
// bills once per GB with a minimum of one. Keep this in step with the actor.
|
|
36
|
+
const ACTOR_RUN_MEMORY_MBYTES = 512;
|
|
37
|
+
// How long this wrapper waits for that run, in milliseconds. The actor's own
|
|
38
|
+
// timeout plus two minutes, so the run's own TIMED-OUT status is what the
|
|
39
|
+
// caller sees rather than the wrapper giving up first and reporting nothing.
|
|
40
|
+
const WRAPPER_WAIT_MS = (ACTOR_RUN_TIMEOUT_SECS + 120) * 1000;
|
|
41
|
+
const POLL_INTERVAL_MS = 3000;
|
|
42
|
+
const TERMINAL = new Set(["SUCCEEDED", "FAILED", "TIMED-OUT", "ABORTED", "ABORTING"]);
|
|
43
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
// Shared caller. actorPath is the actor's immutable Apify actor ID (a stable key
|
|
45
|
+
// that survives Store renames). The /v2/acts/{id} endpoint accepts it directly,
|
|
46
|
+
// so a Store rename never breaks these calls.
|
|
47
|
+
//
|
|
48
|
+
// START AND POLL, NOT RUN-SYNC. Apify's synchronous endpoints carry a platform
|
|
49
|
+
// ceiling of 300 seconds on the HTTP wait itself and answer 408 past it whatever
|
|
50
|
+
// the timeout parameter says, so a long run reads as a timeout even though the
|
|
51
|
+
// actor goes on to finish. Starting the run, polling it to a terminal status and
|
|
52
|
+
// then reading the dataset is the only way to wait as long as the actor needs.
|
|
53
|
+
//
|
|
54
|
+
// The token is read here rather than at module load, so the tool registers
|
|
55
|
+
// unconditionally and a server started without APIFY_TOKEN still advertises its
|
|
56
|
+
// capabilities instead of reporting none.
|
|
31
57
|
async function runActor(actorPath, actorLabel, input) {
|
|
58
|
+
const APIFY_TOKEN = process.env.APIFY_TOKEN;
|
|
32
59
|
if (!APIFY_TOKEN) {
|
|
33
60
|
return { isError: true, content: [{ type: "text", text: "APIFY_TOKEN is not set. Create a token at https://console.apify.com/account/integrations and set it as the APIFY_TOKEN environment variable." }] };
|
|
34
61
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const url = `https://api.apify.com/v2/acts/${actorPath}/run-sync-get-dataset-items?timeout=300&memory=512`;
|
|
42
|
-
let response;
|
|
43
|
-
try {
|
|
44
|
-
response = await fetch(url, {
|
|
45
|
-
method: "POST",
|
|
46
|
-
headers: {
|
|
47
|
-
Authorization: `Bearer ${APIFY_TOKEN}`,
|
|
48
|
-
"Content-Type": "application/json",
|
|
49
|
-
"User-Agent": USER_AGENT,
|
|
50
|
-
},
|
|
51
|
-
body: JSON.stringify(input),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
-
return { isError: true, content: [{ type: "text", text: `Could not reach the Apify API: ${message}` }] };
|
|
57
|
-
}
|
|
58
|
-
if (!response.ok) {
|
|
62
|
+
const headers = {
|
|
63
|
+
Authorization: `Bearer ${APIFY_TOKEN}`,
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"User-Agent": USER_AGENT,
|
|
66
|
+
};
|
|
67
|
+
const httpError = async (response) => {
|
|
59
68
|
let detail = "";
|
|
60
69
|
try {
|
|
61
70
|
const body = (await response.json());
|
|
@@ -65,27 +74,107 @@ async function runActor(actorPath, actorLabel, input) {
|
|
|
65
74
|
catch {
|
|
66
75
|
detail = "";
|
|
67
76
|
}
|
|
68
|
-
let message;
|
|
69
77
|
switch (response.status) {
|
|
78
|
+
case 400:
|
|
79
|
+
return `The ${actorLabel} run was rejected as invalid input.${detail}`;
|
|
70
80
|
case 401:
|
|
71
|
-
|
|
72
|
-
break;
|
|
81
|
+
return "Invalid Apify token. Check your APIFY_TOKEN environment variable.";
|
|
73
82
|
case 402:
|
|
74
|
-
|
|
75
|
-
break;
|
|
76
|
-
case 408:
|
|
77
|
-
message = `The ${actorLabel} run timed out after 300 seconds. Try again, or run the actor on Apify directly for longer jobs.`;
|
|
78
|
-
break;
|
|
83
|
+
return "Insufficient Apify credits. Check your account balance at https://console.apify.com/billing";
|
|
79
84
|
default:
|
|
80
|
-
|
|
85
|
+
return `Apify request to ${actorLabel} failed with status ${response.status}.${detail}`;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// 1. Start the run.
|
|
89
|
+
let started;
|
|
90
|
+
try {
|
|
91
|
+
started = await fetch(`https://api.apify.com/v2/acts/${actorPath}/runs?timeout=${ACTOR_RUN_TIMEOUT_SECS}&memory=${ACTOR_RUN_MEMORY_MBYTES}`, { method: "POST", headers, body: JSON.stringify(input) });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
return { isError: true, content: [{ type: "text", text: `Could not reach the Apify API: ${message}` }] };
|
|
96
|
+
}
|
|
97
|
+
if (!started.ok) {
|
|
98
|
+
return { isError: true, content: [{ type: "text", text: await httpError(started) }] };
|
|
99
|
+
}
|
|
100
|
+
let run;
|
|
101
|
+
try {
|
|
102
|
+
run = (await started.json()).data ?? {};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
return { isError: true, content: [{ type: "text", text: `The ${actorLabel} run start returned a response that could not be parsed: ${message}` }] };
|
|
107
|
+
}
|
|
108
|
+
const runId = run.id;
|
|
109
|
+
if (!runId) {
|
|
110
|
+
return { isError: true, content: [{ type: "text", text: `The ${actorLabel} run start returned no run id, so there is nothing to wait for.` }] };
|
|
111
|
+
}
|
|
112
|
+
// 2. Poll to a terminal status.
|
|
113
|
+
const deadline = Date.now() + WRAPPER_WAIT_MS;
|
|
114
|
+
let status = run.status ?? "READY";
|
|
115
|
+
let datasetId = run.defaultDatasetId;
|
|
116
|
+
while (!TERMINAL.has(status)) {
|
|
117
|
+
if (Date.now() >= deadline) {
|
|
118
|
+
return {
|
|
119
|
+
isError: true,
|
|
120
|
+
content: [{ type: "text", text: `The ${actorLabel} run ${runId} was still ${status} after ${Math.round(WRAPPER_WAIT_MS / 1000)} seconds and this call stopped waiting. The run itself is still on Apify: read it at https://console.apify.com/actors/runs/${runId}` }],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
await sleep(POLL_INTERVAL_MS);
|
|
124
|
+
let poll;
|
|
125
|
+
try {
|
|
126
|
+
poll = await fetch(`https://api.apify.com/v2/actor-runs/${runId}`, { headers });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
return { isError: true, content: [{ type: "text", text: `Lost contact with the Apify API while waiting for ${actorLabel} run ${runId}: ${message}` }] };
|
|
131
|
+
}
|
|
132
|
+
if (!poll.ok) {
|
|
133
|
+
return { isError: true, content: [{ type: "text", text: await httpError(poll) }] };
|
|
81
134
|
}
|
|
82
|
-
|
|
135
|
+
const body = (await poll.json());
|
|
136
|
+
status = body.data?.status ?? status;
|
|
137
|
+
datasetId = body.data?.defaultDatasetId ?? datasetId;
|
|
138
|
+
}
|
|
139
|
+
// 3. A run that did not succeed is a failure the caller must see, never an
|
|
140
|
+
// empty success. Surfacing it here is what keeps a crashed run from reading
|
|
141
|
+
// as "no results found".
|
|
142
|
+
if (status !== "SUCCEEDED") {
|
|
143
|
+
return {
|
|
144
|
+
isError: true,
|
|
145
|
+
content: [{ type: "text", text: `The ${actorLabel} run did not succeed (run ID: ${runId}, status: ${status}).` }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (!datasetId) {
|
|
149
|
+
return { isError: true, content: [{ type: "text", text: `The ${actorLabel} run ${runId} succeeded but reported no dataset, so there is nothing to return.` }] };
|
|
150
|
+
}
|
|
151
|
+
// 4. Read the dataset.
|
|
152
|
+
let ds;
|
|
153
|
+
try {
|
|
154
|
+
ds = await fetch(`https://api.apify.com/v2/datasets/${datasetId}/items?format=json`, { headers });
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
return { isError: true, content: [{ type: "text", text: `Could not read the ${actorLabel} dataset: ${message}` }] };
|
|
159
|
+
}
|
|
160
|
+
if (!ds.ok) {
|
|
161
|
+
return { isError: true, content: [{ type: "text", text: await httpError(ds) }] };
|
|
162
|
+
}
|
|
163
|
+
let items;
|
|
164
|
+
try {
|
|
165
|
+
items = await ds.json();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
return { isError: true, content: [{ type: "text", text: `The ${actorLabel} run returned a response that could not be parsed: ${message}` }] };
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(items)) {
|
|
172
|
+
const asObj = items;
|
|
173
|
+
const detail = asObj?.error?.message
|
|
174
|
+
? `${asObj.error.message}`
|
|
175
|
+
: JSON.stringify(items);
|
|
176
|
+
return { isError: true, content: [{ type: "text", text: `The ${actorLabel} run did not return a dataset. ${detail}` }] };
|
|
83
177
|
}
|
|
84
|
-
// A 2xx normally carries the dataset array. Pass actor output through
|
|
85
|
-
// unchanged: the wrapper must never reinterpret a status field, because
|
|
86
|
-
// not_extractable, blocked and not_found are different answers and collapsing
|
|
87
|
-
// them is exactly the defect the actor was built to avoid.
|
|
88
|
-
const items = await response.json();
|
|
89
178
|
return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] };
|
|
90
179
|
}
|
|
91
180
|
const server = new McpServer({
|
|
@@ -115,22 +204,22 @@ server.registerTool("scan_github_organization_signals", {
|
|
|
115
204
|
.describe("Optional. If you already know the organization login, for example \"stripe\", put it here and the actor skips discovery entirely and goes straight to the API, which is faster and spends fewer of your rate limited requests."),
|
|
116
205
|
includeRepoDetail: z.boolean()
|
|
117
206
|
.optional()
|
|
118
|
-
.describe("When
|
|
207
|
+
.describe("When true (default) the organization repositories are read and the language, star, activity and SDK signals are derived from them. Set false to return the organization record only, which is one request instead of several and is much friendlier to the unauthenticated rate limit."),
|
|
119
208
|
includeContributorEstimate: z.boolean()
|
|
120
209
|
.optional()
|
|
121
|
-
.describe("When
|
|
210
|
+
.describe("When true the actor spends one extra request to read the contributor count of the organization most starred repository, as a floor on the size of its public engineering surface. Default false, because one extra request per company is real money against a 60 per hour unauthenticated budget."),
|
|
122
211
|
activeWindowDays: z.enum(["30", "90", "180", "365"])
|
|
123
212
|
.optional()
|
|
124
|
-
.describe("How recently a repository must have been pushed to count as active. 90 days by default. This changes what \"active\" means on the row, so pick the window your own definition of an engaged engineering team uses.
|
|
213
|
+
.describe("How recently a repository must have been pushed to count as active. 90 days by default. This changes what \"active\" means on the row, so pick the window your own definition of an engaged engineering team uses."),
|
|
125
214
|
repoPageBudget: z.enum(["1", "2", "3", "5"])
|
|
126
215
|
.optional()
|
|
127
|
-
.describe("How many pages of 100 repositories to read for a large organization. This is a cost and completeness dial, not a change of answer: the row always reports how many repositories were actually sampled and whether the sample is complete.
|
|
216
|
+
.describe("How many pages of 100 repositories to read for a large organization. This is a cost and completeness dial, not a change of answer: the row always reports how many repositories were actually sampled and whether the sample is complete."),
|
|
128
217
|
githubToken: z.string()
|
|
129
218
|
.optional()
|
|
130
|
-
.describe("YOUR OWN GitHub personal access token, free to create at github.com/settings/tokens with no scopes at all for public data. OPTIONAL: without it the actor runs at GitHub 60 requests per hour, which is enough for a handful of companies and not enough for a list. With it the limit is 5,000 per hour.
|
|
219
|
+
.describe("YOUR OWN GitHub personal access token, free to create at github.com/settings/tokens with no scopes at all for public data. OPTIONAL: without it the actor runs at GitHub 60 requests per hour, which is enough for a handful of companies and not enough for a list. With it the limit is 5,000 per hour. A token passed here travels in the tool call and stays in the chat transcript, so prefer setting GITHUB_TOKEN in this server's environment: when this argument is omitted the server sends that variable instead."),
|
|
131
220
|
skipCache: z.boolean()
|
|
132
221
|
.optional()
|
|
133
|
-
.describe("When
|
|
222
|
+
.describe("When false (default) a successful lookup is cached for seven days and reused, which costs you nothing on a repeated run. Set true to force a fresh fetch."),
|
|
134
223
|
},
|
|
135
224
|
}, async ({ company_domain, company_name, github_org, includeRepoDetail, includeContributorEstimate, activeWindowDays, repoPageBudget, githubToken, skipCache }) => {
|
|
136
225
|
return runActor("ahF8RRpv5mzPao5CN", "GitHub Organization Signal Scanner", compact({
|
|
@@ -141,7 +230,7 @@ server.registerTool("scan_github_organization_signals", {
|
|
|
141
230
|
includeContributorEstimate: boolToString(includeContributorEstimate),
|
|
142
231
|
activeWindowDays,
|
|
143
232
|
repoPageBudget,
|
|
144
|
-
githubToken,
|
|
233
|
+
githubToken: githubToken ?? process.env.GITHUB_TOKEN,
|
|
145
234
|
skipCache: boolToString(skipCache),
|
|
146
235
|
}));
|
|
147
236
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mambalabsdev/mcp-github-organization-signal-scanner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for the Mamba Labs GitHub Organization Signal Scanner actor on Apify. Resolve a company domain to its GitHub organization with repo, language and activity signals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc && chmod +x build/index.js",
|
|
17
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
17
18
|
"prepublishOnly": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
@@ -38,11 +39,11 @@
|
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
41
|
-
"zod": "^
|
|
42
|
+
"zod": "^4.5.4"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^
|
|
45
|
-
"typescript": "
|
|
45
|
+
"@types/node": "^26.4.1",
|
|
46
|
+
"typescript": "5.9.3"
|
|
46
47
|
},
|
|
47
48
|
"mcpName": "com.mambabuilt/mcp-github-organization-signal-scanner"
|
|
48
49
|
}
|