@knownagents/sdk 1.11.0 → 2.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 +92 -65
- package/dist/KnownAgents.d.ts +74 -0
- package/dist/KnownAgents.d.ts.map +1 -0
- package/dist/KnownAgents.js +242 -0
- package/dist/commerce.d.ts +28 -0
- package/dist/commerce.d.ts.map +1 -0
- package/dist/commerce.js +51 -0
- package/dist/headers.d.ts +6 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +54 -0
- package/dist/index.d.ts +2 -156
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -253
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +72 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/package.json +14 -1
- package/src/KnownAgents.ts +275 -0
- package/src/headers.ts +56 -0
- package/src/index.ts +8 -353
- package/src/paths.ts +72 -0
- package/src/types.ts +109 -0
- package/tsconfig.json +0 -16
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentType = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Agent types that can be controlled with robots.txt rules.
|
|
6
|
+
*/
|
|
7
|
+
var AgentType;
|
|
8
|
+
(function (AgentType) {
|
|
9
|
+
AgentType["AIAgent"] = "AI Agent";
|
|
10
|
+
AgentType["AIAssistant"] = "AI Assistant";
|
|
11
|
+
AgentType["AICodingAgent"] = "AI Coding Agent";
|
|
12
|
+
AgentType["AIDataProvider"] = "AI Data Provider";
|
|
13
|
+
AgentType["AIDataScraper"] = "AI Data Scraper";
|
|
14
|
+
AgentType["AISearchCrawler"] = "AI Search Crawler";
|
|
15
|
+
AgentType["Archiver"] = "Archiver";
|
|
16
|
+
AgentType["AutomatedAgent"] = "Automated Agent";
|
|
17
|
+
AgentType["DeveloperHelper"] = "Developer Helper";
|
|
18
|
+
AgentType["Fetcher"] = "Fetcher";
|
|
19
|
+
AgentType["IntelligenceGatherer"] = "Intelligence Gatherer";
|
|
20
|
+
AgentType["Scraper"] = "Scraper";
|
|
21
|
+
AgentType["SEOCrawler"] = "SEO Crawler";
|
|
22
|
+
AgentType["SearchEngineCrawler"] = "Search Engine Crawler";
|
|
23
|
+
AgentType["SecurityScanner"] = "Security Scanner";
|
|
24
|
+
AgentType["UndocumentedAIAgent"] = "Undocumented AI Agent";
|
|
25
|
+
})(AgentType || (exports.AgentType = AgentType = {}));
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knownagents/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The official Node.js SDK for Known Agents: AI agent analytics (bot traffic), LLM referral tracking, automatic robots.txt, agent verification",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
8
12
|
"keywords": [
|
|
9
13
|
"ai analytics",
|
|
10
14
|
"bot traffic",
|
|
@@ -16,6 +20,12 @@
|
|
|
16
20
|
"bot detection",
|
|
17
21
|
"crawler detection",
|
|
18
22
|
"llm analytics",
|
|
23
|
+
"mcp analytics",
|
|
24
|
+
"mcp server analytics",
|
|
25
|
+
"model context protocol",
|
|
26
|
+
"agentic commerce protocol",
|
|
27
|
+
"universal commerce protocol",
|
|
28
|
+
"commerce analytics",
|
|
19
29
|
"robots.txt",
|
|
20
30
|
"automatic robots.txt",
|
|
21
31
|
"robots.txt generator",
|
|
@@ -54,5 +64,8 @@
|
|
|
54
64
|
"devDependencies": {
|
|
55
65
|
"@types/node": "^22.19.17",
|
|
56
66
|
"typescript": "^5.8.2"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
57
70
|
}
|
|
58
71
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "http"
|
|
2
|
+
import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
|
|
3
|
+
import { getFilteredHeaders, getIsACPCall, getIsMCPCall, getIsUCPCall } from "./headers"
|
|
4
|
+
import { getIsPathExcluded } from "./paths"
|
|
5
|
+
import type {
|
|
6
|
+
AgentType,
|
|
7
|
+
IdentificationRequest,
|
|
8
|
+
IdentificationResult,
|
|
9
|
+
TrackPageviewOrRESTCallOptions,
|
|
10
|
+
VisitRequest
|
|
11
|
+
} from "./types"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Server-side client for Known Agents analytics, robots.txt generation, and agent identification.
|
|
15
|
+
*/
|
|
16
|
+
export class KnownAgents {
|
|
17
|
+
private readonly accessToken: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new instance of the Known Agents client.
|
|
21
|
+
*
|
|
22
|
+
* @param accessToken - Your project's access token.
|
|
23
|
+
*/
|
|
24
|
+
constructor(accessToken: string) {
|
|
25
|
+
this.accessToken = accessToken
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tracks a single non-MCP pageview or REST API call for analytics.
|
|
30
|
+
* The visit is sent after the response finishes so status, headers, and duration are included.
|
|
31
|
+
* This method is non-blocking and handles errors internally.
|
|
32
|
+
*
|
|
33
|
+
* @param request - The incoming Node.js HTTP request.
|
|
34
|
+
* @param response - The outgoing Node.js HTTP response.
|
|
35
|
+
* @param options - Optional ACP or UCP response metadata for REST API calls.
|
|
36
|
+
*/
|
|
37
|
+
trackPageviewOrRESTCall(request: IncomingMessage, response: ServerResponse, options?: TrackPageviewOrRESTCallOptions): void {
|
|
38
|
+
const isMCPCall = getIsMCPCall(request.headers)
|
|
39
|
+
const path = request.url
|
|
40
|
+
const method = request.method
|
|
41
|
+
|
|
42
|
+
if (isMCPCall || !path || getIsPathExcluded(path) || !method) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const created = new Date()
|
|
47
|
+
|
|
48
|
+
const sendVisit = () => {
|
|
49
|
+
const responseDurationInMilliseconds = Date.now() - created.getTime()
|
|
50
|
+
|
|
51
|
+
const visitRequest: VisitRequest = {
|
|
52
|
+
request_path: path,
|
|
53
|
+
request_method: method,
|
|
54
|
+
request_headers: request.headers,
|
|
55
|
+
response_status_code: response?.statusCode,
|
|
56
|
+
response_headers: response.getHeaders(),
|
|
57
|
+
response_duration_in_milliseconds: responseDurationInMilliseconds,
|
|
58
|
+
acp_response_body: options?.acpResponseBody,
|
|
59
|
+
ucp_response_body: options?.ucpResponseBody,
|
|
60
|
+
created: created.toISOString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.trackVisits([visitRequest])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (response.writableEnded) {
|
|
67
|
+
sendVisit()
|
|
68
|
+
} else {
|
|
69
|
+
response.once("finish", sendVisit)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Tracks a single MCP call handled by `StreamableHTTPServerTransport`.
|
|
75
|
+
* Call this after connecting the transport and before the transport processes the request so request and response metadata can be captured.
|
|
76
|
+
* Do not use this with transport instances shared across concurrent requests; use `trackVisits()` instead.
|
|
77
|
+
* This method is non-blocking and handles errors internally.
|
|
78
|
+
*
|
|
79
|
+
* @param request - The incoming Node.js HTTP request.
|
|
80
|
+
* @param response - The outgoing Node.js HTTP response.
|
|
81
|
+
* @param transport - The MCP streamable HTTP transport handling the request.
|
|
82
|
+
*/
|
|
83
|
+
trackMCPCall(request: IncomingMessage, response: ServerResponse, transport: StreamableHTTPServerTransport): void {
|
|
84
|
+
const path = request.url
|
|
85
|
+
const method = request.method
|
|
86
|
+
|
|
87
|
+
if (!path || getIsPathExcluded(path) || !method) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const created = new Date()
|
|
92
|
+
|
|
93
|
+
let mcpRequestBody: any
|
|
94
|
+
let mcpResponseBody: any
|
|
95
|
+
|
|
96
|
+
const originalTransportOnMessage = transport.onmessage
|
|
97
|
+
transport.onmessage = (message, extra): void => {
|
|
98
|
+
mcpRequestBody = message
|
|
99
|
+
originalTransportOnMessage?.(message, extra)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const originalTransportSend = transport.send
|
|
103
|
+
transport.send = async (message, options): Promise<void> => {
|
|
104
|
+
mcpResponseBody = message
|
|
105
|
+
await originalTransportSend.call(transport, message, options)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const restoreTransport = () => {
|
|
109
|
+
transport.onmessage = originalTransportOnMessage
|
|
110
|
+
transport.send = originalTransportSend
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sendVisit = () => {
|
|
114
|
+
restoreTransport()
|
|
115
|
+
|
|
116
|
+
if (!mcpRequestBody || !mcpResponseBody) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const responseDurationInMilliseconds = Date.now() - created.getTime()
|
|
121
|
+
const mcpResponseResult = mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result
|
|
122
|
+
|
|
123
|
+
const visitRequest: VisitRequest = {
|
|
124
|
+
request_path: path,
|
|
125
|
+
request_method: method,
|
|
126
|
+
request_headers: request.headers,
|
|
127
|
+
response_status_code: response?.statusCode,
|
|
128
|
+
response_headers: response.getHeaders(),
|
|
129
|
+
response_duration_in_milliseconds: responseDurationInMilliseconds,
|
|
130
|
+
mcp_request_method: mcpRequestBody.method,
|
|
131
|
+
mcp_request_tool_name: mcpRequestBody.params?.name,
|
|
132
|
+
mcp_request_client_info_name: mcpRequestBody.params?.clientInfo?.name,
|
|
133
|
+
mcp_request_client_info_version: mcpRequestBody.params?.clientInfo?.version,
|
|
134
|
+
mcp_response_result_is_error: mcpResponseBody.result?.isError,
|
|
135
|
+
mcp_response_error_code: mcpResponseBody.error?.code,
|
|
136
|
+
mcp_response_error_message: mcpResponseBody.error?.message,
|
|
137
|
+
acp_response_body: getIsACPCall(request.headers) ? mcpResponseResult : undefined,
|
|
138
|
+
ucp_response_body: getIsUCPCall(request.headers) ? mcpResponseResult : undefined,
|
|
139
|
+
created: created.toISOString()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.trackVisits([visitRequest])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (response.writableEnded) {
|
|
146
|
+
sendVisit()
|
|
147
|
+
} else {
|
|
148
|
+
response.once("finish", sendVisit)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Tracks multiple visits in a batch for analytics.
|
|
154
|
+
* This method is non-blocking and handles errors internally.
|
|
155
|
+
* Recommended for high-traffic websites - batch visits and send periodically (e.g. every 30 seconds).
|
|
156
|
+
*
|
|
157
|
+
* @param requests - An array of visits.
|
|
158
|
+
*/
|
|
159
|
+
trackVisits(requests: VisitRequest[]): void {
|
|
160
|
+
const filteredRequests = requests
|
|
161
|
+
.filter((request) => {
|
|
162
|
+
return !getIsPathExcluded(request.request_path)
|
|
163
|
+
})
|
|
164
|
+
.map((request) => {
|
|
165
|
+
return {
|
|
166
|
+
...request,
|
|
167
|
+
request_headers: getFilteredHeaders(request.request_headers),
|
|
168
|
+
response_headers: request.response_headers ? getFilteredHeaders(request.response_headers) : undefined,
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (filteredRequests.length === 0) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fetch("https://api.knownagents.com/visits", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
"Authorization": `Bearer ${this.accessToken}`,
|
|
180
|
+
"Content-Type": "application/json"
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify(filteredRequests)
|
|
183
|
+
}).catch(error => {
|
|
184
|
+
console.error(`Known Agents failed to track visits: ${error.message}`)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generates a robots.txt file that disallows the specified agent types.
|
|
190
|
+
* The generated file stays up-to-date with all current and future agents in the specified categories.
|
|
191
|
+
* Cache and serve the result at your website's `/robots.txt` endpoint.
|
|
192
|
+
*
|
|
193
|
+
* @param agentTypes - An array of agent types to disallow.
|
|
194
|
+
* @param disallow - The path to disallow (default: "/" for all paths).
|
|
195
|
+
* @returns A promise that resolves to the generated robots.txt content as a string.
|
|
196
|
+
* @throws {Error} If the API call fails or returns a non-200 status.
|
|
197
|
+
*/
|
|
198
|
+
async generateRobotsTXT(agentTypes: AgentType[], disallow: string = "/"): Promise<string> {
|
|
199
|
+
const response = await fetch("https://api.knownagents.com/robots-txts", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
"Authorization": `Bearer ${this.accessToken}`,
|
|
203
|
+
"Content-Type": "application/json"
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
agent_types: agentTypes,
|
|
207
|
+
disallow: disallow
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
if (response.ok) {
|
|
212
|
+
return await response.text()
|
|
213
|
+
} else {
|
|
214
|
+
throw new Error(`Known Agents failed to generate robots.txt: ${response.status} ${response.statusText}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Identifies and verifies a single agent from an HTTP request.
|
|
220
|
+
* Uses Web Bot Auth (HTTP message signatures), IP matching, or other available methods.
|
|
221
|
+
*
|
|
222
|
+
* @param request - The incoming Node.js HTTP request.
|
|
223
|
+
* @returns A promise that resolves to the identification result.
|
|
224
|
+
* @throws {Error} If the API call fails or returns a non-200 status.
|
|
225
|
+
*/
|
|
226
|
+
async identifyAgent(request: IncomingMessage): Promise<IdentificationResult> {
|
|
227
|
+
const response = await fetch("https://api.knownagents.com/agent-identifications", {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Authorization": `Bearer ${this.accessToken}`,
|
|
231
|
+
"Content-Type": "application/json"
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
request_headers: getFilteredHeaders(request.headers)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
if (response.ok) {
|
|
239
|
+
return await response.json()
|
|
240
|
+
} else {
|
|
241
|
+
throw new Error(`Known Agents failed to identify agent: ${response.status} ${response.statusText}`)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Identifies and verifies multiple agents from HTTP requests in a batch.
|
|
247
|
+
* Uses Web Bot Auth (HTTP message signatures), IP matching, or other available methods.
|
|
248
|
+
*
|
|
249
|
+
* @param requests - An array of identification requests.
|
|
250
|
+
* @returns A promise that resolves to an array of identification results.
|
|
251
|
+
* @throws {Error} If the API call fails or returns a non-200 status.
|
|
252
|
+
*/
|
|
253
|
+
async identifyAgents(requests: IdentificationRequest[]): Promise<IdentificationResult[]> {
|
|
254
|
+
const response = await fetch("https://api.knownagents.com/agent-identifications", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
"Authorization": `Bearer ${this.accessToken}`,
|
|
258
|
+
"Content-Type": "application/json"
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify(requests.map(request => {
|
|
261
|
+
return {
|
|
262
|
+
...request,
|
|
263
|
+
request_headers: getFilteredHeaders(request.request_headers),
|
|
264
|
+
}
|
|
265
|
+
}))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (response.ok) {
|
|
269
|
+
return await response.json()
|
|
270
|
+
} else {
|
|
271
|
+
throw new Error(`Known Agents failed to identify agents: ${response.status} ${response.statusText}`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
}
|
package/src/headers.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"
|
|
2
|
+
|
|
3
|
+
const filteredHeaderNames = [
|
|
4
|
+
"authorization",
|
|
5
|
+
"proxy-authorization",
|
|
6
|
+
"cookie",
|
|
7
|
+
"set-cookie",
|
|
8
|
+
"x-api-key",
|
|
9
|
+
"api-key",
|
|
10
|
+
"apikey",
|
|
11
|
+
"x-auth-token",
|
|
12
|
+
"x-access-token",
|
|
13
|
+
"x-id-token",
|
|
14
|
+
"x-refresh-token",
|
|
15
|
+
"x-session-token",
|
|
16
|
+
"session-token",
|
|
17
|
+
"csrf-token",
|
|
18
|
+
"x-csrf-token",
|
|
19
|
+
"xsrf-token",
|
|
20
|
+
"x-xsrf-token",
|
|
21
|
+
"x-amz-security-token",
|
|
22
|
+
"x-goog-api-key",
|
|
23
|
+
"x-client-secret",
|
|
24
|
+
"client-secret",
|
|
25
|
+
"private-token"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export function getFilteredHeaders<Headers extends IncomingHttpHeaders | OutgoingHttpHeaders>(headers: Headers): Headers {
|
|
29
|
+
const filteredHeaders = { ...headers }
|
|
30
|
+
|
|
31
|
+
Object.keys(filteredHeaders).forEach((key) => {
|
|
32
|
+
if (filteredHeaderNames.includes(key.toLowerCase())) {
|
|
33
|
+
delete filteredHeaders[key]
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return filteredHeaders
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getIsMCPCall(headers: IncomingHttpHeaders): boolean {
|
|
41
|
+
return Object.keys(headers).some((key) => {
|
|
42
|
+
return key.toLowerCase() === "mcp-protocol-version"
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getIsUCPCall(headers: IncomingHttpHeaders): boolean {
|
|
47
|
+
return Object.keys(headers).some((key) => {
|
|
48
|
+
return key.toLowerCase() === "ucp-agent"
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getIsACPCall(headers: IncomingHttpHeaders): boolean {
|
|
53
|
+
return !getIsUCPCall(headers) && Object.keys(headers).some((key) => {
|
|
54
|
+
return key.toLowerCase() === "api-version"
|
|
55
|
+
})
|
|
56
|
+
}
|