company-dossier 0.1.0 → 0.2.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 +41 -0
- package/dist/cli.js +57 -7
- package/dist/index.js +57 -7
- package/dist/mcp-http.js +1612 -0
- package/dist/mcp-server.js +1442 -0
- package/dist/mcp.js +99 -37
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -99,6 +99,47 @@ Tool input:
|
|
|
99
99
|
{ "target": "acme.com", "sections": ["overview", "tech", "risk"] }
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
## Remote MCP server
|
|
103
|
+
|
|
104
|
+
In addition to the stdio server, `company-dossier` ships a **remote (HTTP) MCP
|
|
105
|
+
server** over the MCP Streamable HTTP transport, exposing the same single tool,
|
|
106
|
+
`build_dossier`. This is what you deploy so hosted assistants (ChatGPT Apps SDK,
|
|
107
|
+
Claude connectors) can reach it over the network.
|
|
108
|
+
|
|
109
|
+
Run it locally:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx company-dossier-mcp-http
|
|
113
|
+
# listening on http://0.0.0.0:8787 (override with PORT=...)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Endpoints: `POST/GET/DELETE /mcp` for the MCP session and `GET /health`
|
|
117
|
+
(returns `{"status":"ok"}`). It listens on `process.env.PORT || 8787`.
|
|
118
|
+
|
|
119
|
+
### Hosted endpoint
|
|
120
|
+
|
|
121
|
+
Deploy it (see [`deploy/README.md`](deploy/README.md) for one-command steps to
|
|
122
|
+
Render, Fly.io, or any Docker host) and point a subdomain at it. The hosted MCP
|
|
123
|
+
endpoint is then:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
https://mcp.companydossier.lol/mcp
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Claude connectors / Claude Desktop (custom connector):** add a remote MCP
|
|
130
|
+
server with URL `https://mcp.companydossier.lol/mcp`. The `build_dossier` tool
|
|
131
|
+
becomes available.
|
|
132
|
+
|
|
133
|
+
**ChatGPT (Apps SDK / connectors):** add an MCP server pointing at
|
|
134
|
+
`https://mcp.companydossier.lol/mcp`; ChatGPT discovers and calls the
|
|
135
|
+
`build_dossier` tool.
|
|
136
|
+
|
|
137
|
+
Tool input (same as the stdio server):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{ "target": "acme.com", "sections": ["overview", "tech", "risk"] }
|
|
141
|
+
```
|
|
142
|
+
|
|
102
143
|
## Output
|
|
103
144
|
|
|
104
145
|
- A `<Company> DOSSIER/` folder with `README.md`, nine numbered markdown
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as dns } from "dns";
|
|
|
6
6
|
// src/utils.ts
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
+
import { lookup } from "dns/promises";
|
|
9
10
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
10
11
|
function mkdirp(dirPath) {
|
|
11
12
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -22,18 +23,67 @@ function todayISO() {
|
|
|
22
23
|
function titleCase(str) {
|
|
23
24
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
24
25
|
}
|
|
26
|
+
function isPrivateIp(ip) {
|
|
27
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
28
|
+
if (v.includes(":")) {
|
|
29
|
+
if (v === "::1" || v === "::") return true;
|
|
30
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
31
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
32
|
+
if (m) return isPrivateIp(m[1]);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const p = v.split(".").map(Number);
|
|
36
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
37
|
+
const [a, b] = p;
|
|
38
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
39
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
40
|
+
if (a === 192 && b === 168) return true;
|
|
41
|
+
if (a === 169 && b === 254) return true;
|
|
42
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
43
|
+
if (a === 192 && b === 0) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
async function assertPublicUrl(url) {
|
|
47
|
+
let u;
|
|
48
|
+
try {
|
|
49
|
+
u = new URL(url);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("Invalid URL");
|
|
52
|
+
}
|
|
53
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
54
|
+
const host = u.hostname.toLowerCase();
|
|
55
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
56
|
+
throw new Error(`Blocked host: ${host}`);
|
|
57
|
+
}
|
|
58
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
59
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const addrs = await lookup(host, { all: true });
|
|
63
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
64
|
+
}
|
|
25
65
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
26
66
|
const controller = new AbortController();
|
|
27
67
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
28
68
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
let current = url;
|
|
70
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
71
|
+
await assertPublicUrl(current);
|
|
72
|
+
const resp = await fetch(current, {
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
redirect: "manual",
|
|
75
|
+
headers: { "User-Agent": USER_AGENT }
|
|
76
|
+
});
|
|
77
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
78
|
+
const loc = resp.headers.get("location");
|
|
79
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
80
|
+
current = new URL(loc, current).toString();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
84
|
+
return await resp.text();
|
|
35
85
|
}
|
|
36
|
-
|
|
86
|
+
throw new Error("Too many redirects");
|
|
37
87
|
} finally {
|
|
38
88
|
clearTimeout(timer);
|
|
39
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path2 from "path";
|
|
|
4
4
|
// src/utils.ts
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
|
+
import { lookup } from "dns/promises";
|
|
7
8
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
8
9
|
function mkdirp(dirPath) {
|
|
9
10
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -23,18 +24,67 @@ function slugify(name) {
|
|
|
23
24
|
function titleCase(str) {
|
|
24
25
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
25
26
|
}
|
|
27
|
+
function isPrivateIp(ip) {
|
|
28
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
29
|
+
if (v.includes(":")) {
|
|
30
|
+
if (v === "::1" || v === "::") return true;
|
|
31
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
32
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
33
|
+
if (m) return isPrivateIp(m[1]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const p = v.split(".").map(Number);
|
|
37
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
38
|
+
const [a, b] = p;
|
|
39
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
40
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
41
|
+
if (a === 192 && b === 168) return true;
|
|
42
|
+
if (a === 169 && b === 254) return true;
|
|
43
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
44
|
+
if (a === 192 && b === 0) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function assertPublicUrl(url) {
|
|
48
|
+
let u;
|
|
49
|
+
try {
|
|
50
|
+
u = new URL(url);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("Invalid URL");
|
|
53
|
+
}
|
|
54
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
55
|
+
const host = u.hostname.toLowerCase();
|
|
56
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
57
|
+
throw new Error(`Blocked host: ${host}`);
|
|
58
|
+
}
|
|
59
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
60
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const addrs = await lookup(host, { all: true });
|
|
64
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
65
|
+
}
|
|
26
66
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
27
67
|
const controller = new AbortController();
|
|
28
68
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
29
69
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
let current = url;
|
|
71
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
72
|
+
await assertPublicUrl(current);
|
|
73
|
+
const resp = await fetch(current, {
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
redirect: "manual",
|
|
76
|
+
headers: { "User-Agent": USER_AGENT }
|
|
77
|
+
});
|
|
78
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
79
|
+
const loc = resp.headers.get("location");
|
|
80
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
81
|
+
current = new URL(loc, current).toString();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
85
|
+
return await resp.text();
|
|
36
86
|
}
|
|
37
|
-
|
|
87
|
+
throw new Error("Too many redirects");
|
|
38
88
|
} finally {
|
|
39
89
|
clearTimeout(timer);
|
|
40
90
|
}
|