@mambalabsdev/mcp-github-organization-signal-scanner 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mamba Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/SECURITY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest published `1.x` release of
6
+ `@mambalabsdev/mcp-company-social-presence-mapper` receives security updates.
7
+
8
+ | Version | Supported |
9
+ | ------- | ------------------ |
10
+ | 1.x | :white_check_mark: |
11
+ | < 1.0 | :x: |
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ Please do not open a public GitHub issue for security reports.
16
+
17
+ Email security reports to **labs@mambamindsetgrowth.com** with:
18
+
19
+ - a description of the issue and its impact,
20
+ - steps to reproduce, and
21
+ - any relevant logs or proof of concept.
22
+
23
+ We aim to acknowledge reports within 3 business days and to provide a remediation
24
+ timeline after triage. Coordinated disclosure is appreciated.
25
+
26
+ ## Handling of credentials
27
+
28
+ This server reads an `APIFY_TOKEN` from the environment and uses it only to call
29
+ the Apify API on your behalf. The token is never logged or written to disk by this
30
+ package. Treat your `APIFY_TOKEN` as a secret and rotate it at
31
+ https://console.apify.com/account/integrations if you believe it has been exposed.
package/build/index.js ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
10
+ // Distinctive UA so Apify run meta.userAgent marks MCP-originated runs.
11
+ const USER_AGENT = `mambalabs-mcp ${pkg.name}@${pkg.version}`;
12
+ const APIFY_TOKEN = process.env.APIFY_TOKEN;
13
+ // Drop undefined values so optional inputs are not sent to the actor at all.
14
+ function compact(obj) {
15
+ const out = {};
16
+ for (const [k, v] of Object.entries(obj)) {
17
+ if (v !== undefined)
18
+ out[k] = v;
19
+ }
20
+ return out;
21
+ }
22
+ // The actor types its switches as strings ("true"/"false") for Clay
23
+ // compatibility, because Clay sends every input as a string and a boolean typed
24
+ // field silently receives "false" and reads it as truthy. The model gets a real
25
+ // boolean and the actor gets the string it validates.
26
+ function boolToString(v) {
27
+ return v === undefined ? undefined : v ? "true" : "false";
28
+ }
29
+ // actorPath is the actor's IMMUTABLE Apify actor id, not its slug, so a Store
30
+ // rename never breaks these calls.
31
+ async function runActor(actorPath, actorLabel, input) {
32
+ if (!APIFY_TOKEN) {
33
+ 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
+ }
35
+ // memory=512 is deliberate and matches the actor's declared
36
+ // defaultRunOptions.memoryMbytes. run-sync-get-dataset-items runs at 2048 MB
37
+ // unless told otherwise, and `apify-actor-start` bills once per GB with a
38
+ // minimum of one, so leaving the default in place would charge the caller
39
+ // more start events per run than the actor asks for. Keep this in step with
40
+ // the actor's defaultRunOptions.
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) {
59
+ let detail = "";
60
+ try {
61
+ const body = (await response.json());
62
+ if (body?.error?.message)
63
+ detail = ` ${body.error.message}`;
64
+ }
65
+ catch {
66
+ detail = "";
67
+ }
68
+ let message;
69
+ switch (response.status) {
70
+ case 401:
71
+ message = "Invalid Apify token. Check your APIFY_TOKEN environment variable.";
72
+ break;
73
+ case 402:
74
+ message = "Insufficient Apify credits. Check your account balance at https://console.apify.com/billing";
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;
79
+ default:
80
+ message = `Apify request to ${actorLabel} failed with status ${response.status}.${detail}`;
81
+ }
82
+ return { isError: true, content: [{ type: "text", text: message }] };
83
+ }
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
+ return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] };
90
+ }
91
+ const server = new McpServer({
92
+ name: "mamba-github-organization-signal-scanner",
93
+ version: pkg.version,
94
+ });
95
+ // GitHub Organization Signal Scanner (immutable actor ID ahF8RRpv5mzPao5CN)
96
+ server.registerTool("scan_github_organization_signals", {
97
+ title: "Scan GitHub Organization Signals",
98
+ description: "Resolve a company domain to its GitHub organization and return 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. Returns one flat Clay ready row. 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. Read only; requires an APIFY_TOKEN and consumes Apify credits per call.",
99
+ annotations: {
100
+ title: "Scan GitHub Organization Signals",
101
+ readOnlyHint: true,
102
+ destructiveHint: false,
103
+ idempotentHint: true,
104
+ openWorldHint: true,
105
+ },
106
+ inputSchema: {
107
+ company_domain: z.string()
108
+ .optional()
109
+ .describe("Bare company domain, for example stripe.com. This is the only required input and it is the join key for every other actor in the fleet."),
110
+ company_name: z.string()
111
+ .optional()
112
+ .describe("Optional but strongly recommended. It is what the identity gate checks a discovered record against, so supplying it is the single cheapest way to reduce wrong matches."),
113
+ github_org: z.string()
114
+ .optional()
115
+ .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
+ includeRepoDetail: z.boolean()
117
+ .optional()
118
+ .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. Sent as a string for Clay compatibility."),
119
+ includeContributorEstimate: z.boolean()
120
+ .optional()
121
+ .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. Sent as a string for Clay compatibility."),
122
+ activeWindowDays: z.enum(["30", "90", "180", "365"])
123
+ .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. Sent as a string for Clay compatibility."),
125
+ repoPageBudget: z.enum(["1", "2", "3", "5"])
126
+ .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. Sent as a string for Clay compatibility."),
128
+ githubToken: z.string()
129
+ .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. It is marked secret, so the value never renders on this page."),
131
+ skipCache: z.boolean()
132
+ .optional()
133
+ .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. Sent as a string for Clay compatibility."),
134
+ },
135
+ }, async ({ company_domain, company_name, github_org, includeRepoDetail, includeContributorEstimate, activeWindowDays, repoPageBudget, githubToken, skipCache }) => {
136
+ return runActor("ahF8RRpv5mzPao5CN", "GitHub Organization Signal Scanner", compact({
137
+ company_domain,
138
+ company_name,
139
+ github_org,
140
+ includeRepoDetail: boolToString(includeRepoDetail),
141
+ includeContributorEstimate: boolToString(includeContributorEstimate),
142
+ activeWindowDays,
143
+ repoPageBudget,
144
+ githubToken,
145
+ skipCache: boolToString(skipCache),
146
+ }));
147
+ });
148
+ const transport = new StdioServerTransport();
149
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mambalabsdev/mcp-github-organization-signal-scanner",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "mcp-github-organization-signal-scanner": "./build/index.js"
8
+ },
9
+ "files": [
10
+ "build",
11
+ "README.md",
12
+ "LICENSE",
13
+ "SECURITY.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc && chmod +x build/index.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "github",
23
+ "developer-tools",
24
+ "open-source",
25
+ "technographics",
26
+ "apify",
27
+ "clay"
28
+ ],
29
+ "author": "Mamba Labs <labs@mambamindsetgrowth.com>",
30
+ "license": "MIT",
31
+ "homepage": "https://apify.com/mambalabs",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/mambalabsdev/mcp-github-organization-signal-scanner"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.12.0",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.14.0",
45
+ "typescript": "^5.6.0"
46
+ },
47
+ "mcpName": "com.mambabuilt/mcp-github-organization-signal-scanner"
48
+ }