@nshipster/sosumi 1.0.6 → 1.0.7
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 +44 -1
- package/package.json +4 -2
- package/public/index.html +3 -1
- package/public/llms.txt +1 -1
- package/public/robots.txt +1 -0
- package/public/webmcp.js +71 -0
- package/src/index.ts +112 -1
- package/src/lib/a2a.ts +90 -0
- package/src/lib/external/fetch.ts +3 -0
- package/src/lib/hig/render.ts +94 -73
- package/src/lib/markdown.ts +115 -0
- package/src/lib/mcp.ts +150 -115
- package/src/lib/reference/render.ts +112 -156
- package/src/lib/search.ts +115 -55
- package/src/lib/types.ts +12 -0
- package/src/lib/url.ts +35 -0
- package/src/lib/webbotauth.ts +261 -0
- package/src/lib/webmcp.ts +38 -0
- package/wrangler.jsonc +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ See [the website](https://sosumi.ai/#clients) for client-specific instructions.
|
|
|
93
93
|
- Returns structured results with titles, URLs, descriptions, breadcrumbs, and tags
|
|
94
94
|
|
|
95
95
|
- `fetchAppleDocumentation` - Fetches Apple Developer documentation and Human Interface Guidelines by path
|
|
96
|
-
- Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', 'swiftui/view', 'design/human-interface-guidelines/foundations/color')
|
|
96
|
+
- Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', '/documentation/swiftui/view', '/design/human-interface-guidelines/foundations/color')
|
|
97
97
|
- Returns content as Markdown
|
|
98
98
|
|
|
99
99
|
- `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions
|
|
@@ -234,6 +234,49 @@ with two environment variables (both newline-delimited):
|
|
|
234
234
|
> Hostname-based private-network checks cannot fully prevent DNS rebinding.
|
|
235
235
|
> Set an explicit `EXTERNAL_DOC_HOST_ALLOWLIST` in production.
|
|
236
236
|
|
|
237
|
+
### Web Bot Auth
|
|
238
|
+
|
|
239
|
+
Sosumi identifies itself
|
|
240
|
+
when it fetches from external Swift-DocC hosts on a user's behalf
|
|
241
|
+
using [Web Bot Auth](https://datatracker.ietf.org/wg/webbotauth/about/),
|
|
242
|
+
so those hosts can cryptographically verify the traffic.
|
|
243
|
+
|
|
244
|
+
- The public key set is published at
|
|
245
|
+
[`/.well-known/http-message-signatures-directory`](https://sosumi.ai/.well-known/http-message-signatures-directory).
|
|
246
|
+
- Requests to external hosts are signed
|
|
247
|
+
with an Ed25519 [HTTP Message Signature (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421)
|
|
248
|
+
and carry `Signature-Agent`, `Signature-Input`, and `Signature` headers.
|
|
249
|
+
|
|
250
|
+
To enable signing on your own deployment,
|
|
251
|
+
provide an Ed25519 private key (as a JSON Web Key)
|
|
252
|
+
via the `WEB_BOT_AUTH_KEY` secret.
|
|
253
|
+
|
|
254
|
+
Generate a key (the JSON Web Key is the format `WEB_BOT_AUTH_KEY` expects),
|
|
255
|
+
then paste the printed value when `wrangler secret put` prompts for it:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
node -e 'crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"]).then(async k=>{const j=await crypto.subtle.exportKey("jwk",k.privateKey);console.log(JSON.stringify({kty:j.kty,crv:j.crv,x:j.x,d:j.d}))})'
|
|
259
|
+
npx wrangler secret put WEB_BOT_AUTH_KEY
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Or generate and upload in a single step,
|
|
263
|
+
which avoids writing the key to disk or shell history:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
node -e 'crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"]).then(async k=>{const j=await crypto.subtle.exportKey("jwk",k.privateKey);console.log(JSON.stringify({kty:j.kty,crv:j.crv,x:j.x,d:j.d}))})' \
|
|
267
|
+
| npx wrangler secret put WEB_BOT_AUTH_KEY
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The matching public key and its `kid` (JWK thumbprint) are derived automatically,
|
|
271
|
+
so no key material lives in the repository.
|
|
272
|
+
Set the optional `SIGNATURE_AGENT` var to override the advertised origin
|
|
273
|
+
(defaults to `https://sosumi.ai`).
|
|
274
|
+
When no key is configured, requests are simply sent unsigned.
|
|
275
|
+
|
|
276
|
+
Cloudflare provides
|
|
277
|
+
[built-in verification](https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/)
|
|
278
|
+
for these signatures.
|
|
279
|
+
|
|
237
280
|
## Development
|
|
238
281
|
|
|
239
282
|
### Testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nshipster/sosumi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Making Apple docs AI-readable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "NSHipster",
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
"hono": "^4.9.4",
|
|
62
62
|
"robots-parser": "^3.0.1",
|
|
63
63
|
"tsx": "^4.21.0",
|
|
64
|
-
"
|
|
64
|
+
"web-bot-auth": "^0.1.3",
|
|
65
|
+
"zod": "^3",
|
|
66
|
+
"zod-to-json-schema": "^3.25.2"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
67
69
|
"@biomejs/biome": "2.2.2",
|
package/public/index.html
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
|
|
37
37
|
<link rel="preload" href="/sosumi.m4a" as="audio" type="audio/mp4">
|
|
38
38
|
|
|
39
|
+
<script src="/webmcp.js" defer></script>
|
|
40
|
+
|
|
39
41
|
<style>
|
|
40
42
|
:root {
|
|
41
43
|
color-scheme: light dark;
|
|
@@ -809,7 +811,7 @@
|
|
|
809
811
|
<dl>
|
|
810
812
|
<dt><code>fetchAppleDocumentation</code></dt>
|
|
811
813
|
<dd><strong>Fetches Apple Developer documentation and Human Interface Guidelines by path</strong>
|
|
812
|
-
<br>Parameters: <code>path</code> (string) - Documentation path (e.g., '/documentation/swift', 'design/human-interface-guidelines/foundations/color')
|
|
814
|
+
<br>Parameters: <code>path</code> (string) - Documentation path (e.g., '/documentation/swift', '/documentation/swiftui/view', '/design/human-interface-guidelines/foundations/color')
|
|
813
815
|
<br><em>Returns content as Markdown</em>
|
|
814
816
|
</dd>
|
|
815
817
|
</dl>
|
package/public/llms.txt
CHANGED
|
@@ -121,7 +121,7 @@ you can run this command to proxy over stdio:
|
|
|
121
121
|
- Returns structured results with titles, URLs, descriptions, breadcrumbs, and tags
|
|
122
122
|
|
|
123
123
|
- `fetchAppleDocumentation` - Fetches Apple Developer documentation and Human Interface Guidelines by path
|
|
124
|
-
- Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', 'design/human-interface-guidelines/foundations/color')
|
|
124
|
+
- Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', '/documentation/swiftui/view', '/design/human-interface-guidelines/foundations/color')
|
|
125
125
|
- Returns content as Markdown
|
|
126
126
|
|
|
127
127
|
- `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions, by video path
|
package/public/robots.txt
CHANGED
package/public/webmcp.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const ctx =
|
|
3
|
+
(typeof navigator !== "undefined" && navigator.modelContext) || document.modelContext
|
|
4
|
+
if (!ctx) return
|
|
5
|
+
if (typeof ctx.registerTool !== "function" && typeof ctx.provideContext !== "function") return
|
|
6
|
+
|
|
7
|
+
async function runHttpTool(http, input) {
|
|
8
|
+
let pathname = http.path
|
|
9
|
+
if (http.pathFrom) {
|
|
10
|
+
const value = String(input[http.pathFrom] ?? "")
|
|
11
|
+
if (http.pathPrefix) {
|
|
12
|
+
pathname = http.pathPrefix + value.replace(/^\//, "")
|
|
13
|
+
} else {
|
|
14
|
+
pathname = value.startsWith("/") ? value : "/" + value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const url = new URL(pathname, location.origin)
|
|
18
|
+
if (http.query) {
|
|
19
|
+
for (const [param, key] of Object.entries(http.query)) {
|
|
20
|
+
url.searchParams.set(param, String(input[key] ?? ""))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(url, { headers: { Accept: "application/json" } })
|
|
24
|
+
const body = await response.text()
|
|
25
|
+
let data
|
|
26
|
+
try {
|
|
27
|
+
data = JSON.parse(body)
|
|
28
|
+
} catch {
|
|
29
|
+
data = body
|
|
30
|
+
}
|
|
31
|
+
const text =
|
|
32
|
+
data && typeof data === "object" && data !== null && "content" in data
|
|
33
|
+
? String(data.content)
|
|
34
|
+
: typeof data === "string"
|
|
35
|
+
? data
|
|
36
|
+
: JSON.stringify(data, null, 2)
|
|
37
|
+
return { content: [{ type: "text", text }] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registerTool(tool) {
|
|
41
|
+
const registration = {
|
|
42
|
+
name: tool.name,
|
|
43
|
+
title: tool.title,
|
|
44
|
+
description: tool.description,
|
|
45
|
+
inputSchema: tool.inputSchema,
|
|
46
|
+
annotations: { readOnlyHint: tool.readOnly },
|
|
47
|
+
execute: (input) => runHttpTool(tool.http, input),
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (typeof ctx.registerTool === "function") {
|
|
51
|
+
ctx.registerTool(registration)
|
|
52
|
+
} else if (typeof ctx.provideContext === "function") {
|
|
53
|
+
ctx.provideContext({ tools: [registration] })
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore duplicate or unsupported registrations.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fetch("/webmcp/manifest.json")
|
|
61
|
+
.then((response) => {
|
|
62
|
+
if (!response.ok) throw new Error("Failed to load WebMCP manifest")
|
|
63
|
+
return response.json()
|
|
64
|
+
})
|
|
65
|
+
.then((tools) => {
|
|
66
|
+
for (const tool of tools) {
|
|
67
|
+
registerTool(tool)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {})
|
|
71
|
+
})()
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { cache } from "hono/cache"
|
|
|
5
5
|
import { cors } from "hono/cors"
|
|
6
6
|
import { HTTPException } from "hono/http-exception"
|
|
7
7
|
import { trimTrailingSlash } from "hono/trailing-slash"
|
|
8
|
+
import { buildAgentCard } from "./lib/a2a"
|
|
8
9
|
import {
|
|
9
10
|
decodeExternalTargetPath,
|
|
10
11
|
ExternalAccessError,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
renderHIGFromJSON,
|
|
20
21
|
renderHIGTableOfContents,
|
|
21
22
|
} from "./lib/hig"
|
|
22
|
-
import { createMcpServer } from "./lib/mcp"
|
|
23
|
+
import { createMcpServer, MCP_SERVER_INFO } from "./lib/mcp"
|
|
23
24
|
import { fetchJSONData, renderFromJSON } from "./lib/reference"
|
|
24
25
|
import { searchAppleDeveloperDocs } from "./lib/search"
|
|
25
26
|
import {
|
|
@@ -32,17 +33,28 @@ import {
|
|
|
32
33
|
} from "./lib/skill"
|
|
33
34
|
import { generateAppleDocUrl, isValidAppleDocUrl, normalizeDocumentationPath } from "./lib/url"
|
|
34
35
|
import { fetchVideoTranscriptMarkdown, TranscriptNotFoundError } from "./lib/video"
|
|
36
|
+
import {
|
|
37
|
+
configureWebBotAuth,
|
|
38
|
+
DIRECTORY_PATH as WEB_BOT_AUTH_DIRECTORY_PATH,
|
|
39
|
+
webBotAuthDirectory,
|
|
40
|
+
} from "./lib/webbotauth"
|
|
41
|
+
import { buildWebMcpManifest } from "./lib/webmcp"
|
|
35
42
|
|
|
36
43
|
interface Env {
|
|
37
44
|
ASSETS: Fetcher
|
|
38
45
|
NODE_ENV: string
|
|
39
46
|
EXTERNAL_DOC_HOST_ALLOWLIST?: string
|
|
40
47
|
EXTERNAL_DOC_HOST_BLOCKLIST?: string
|
|
48
|
+
WEB_BOT_AUTH_KEY?: string
|
|
49
|
+
SIGNATURE_AGENT?: string
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
const app = new Hono<{ Bindings: Env }>()
|
|
44
53
|
|
|
45
54
|
app.use("*", async (c, next) => {
|
|
55
|
+
// Prime Web Bot Auth signing before any route or outbound fetch runs.
|
|
56
|
+
configureWebBotAuth(c.env)
|
|
57
|
+
|
|
46
58
|
await next()
|
|
47
59
|
|
|
48
60
|
// Security headers
|
|
@@ -59,6 +71,19 @@ app.use("*", async (c, next) => {
|
|
|
59
71
|
if (c.env.NODE_ENV === "development") {
|
|
60
72
|
c.header("Cache-Control", "no-store")
|
|
61
73
|
}
|
|
74
|
+
|
|
75
|
+
if (c.req.path === "/") {
|
|
76
|
+
c.header(
|
|
77
|
+
"Link",
|
|
78
|
+
[
|
|
79
|
+
'</.well-known/api-catalog>; rel="api-catalog"',
|
|
80
|
+
'</.well-known/agent-card.json>; rel="service-desc"; type="application/json"',
|
|
81
|
+
'</.well-known/mcp/server-card.json>; rel="service-desc"; type="application/json"',
|
|
82
|
+
'</SKILL.md>; rel="service-doc"',
|
|
83
|
+
'</llms.txt>; rel="alternate"; type="text/markdown"',
|
|
84
|
+
].join(", "),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
62
87
|
})
|
|
63
88
|
|
|
64
89
|
app.use("*", cors())
|
|
@@ -146,6 +171,92 @@ app.all(`/.well-known/agent-skills/${SKILL_NAME}/SKILL.md`, async (c) => {
|
|
|
146
171
|
})
|
|
147
172
|
})
|
|
148
173
|
|
|
174
|
+
app.get(WEB_BOT_AUTH_DIRECTORY_PATH, async (c) => {
|
|
175
|
+
const directory = await webBotAuthDirectory(c.req.url)
|
|
176
|
+
if (!directory) {
|
|
177
|
+
throw new HTTPException(404, { message: "Web Bot Auth key directory is not configured." })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return c.json(directory.body, 200, {
|
|
181
|
+
"Access-Control-Allow-Origin": "*",
|
|
182
|
+
...directory.headers,
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const discoveryHeaders = {
|
|
187
|
+
"Access-Control-Allow-Origin": "*",
|
|
188
|
+
"Cache-Control": "public, max-age=300, s-maxage=600",
|
|
189
|
+
} as const
|
|
190
|
+
|
|
191
|
+
app.get("/.well-known/api-catalog", (c) => {
|
|
192
|
+
const origin = new URL(c.req.url).origin
|
|
193
|
+
|
|
194
|
+
return c.json(
|
|
195
|
+
{
|
|
196
|
+
linkset: [
|
|
197
|
+
{
|
|
198
|
+
anchor: `${origin}/mcp`,
|
|
199
|
+
"service-desc": [
|
|
200
|
+
{
|
|
201
|
+
href: `${origin}/.well-known/mcp/server-card.json`,
|
|
202
|
+
type: "application/json",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
"service-doc": [{ href: `${origin}/SKILL.md`, type: "text/markdown" }],
|
|
206
|
+
status: [{ href: `${origin}/` }],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
anchor: `${origin}/documentation`,
|
|
210
|
+
"service-doc": [{ href: `${origin}/SKILL.md`, type: "text/markdown" }],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
200,
|
|
215
|
+
{
|
|
216
|
+
...discoveryHeaders,
|
|
217
|
+
"Content-Type": "application/linkset+json; charset=utf-8",
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
app.get("/.well-known/mcp/server-card.json", (c) => {
|
|
223
|
+
const origin = new URL(c.req.url).origin
|
|
224
|
+
|
|
225
|
+
return c.json(
|
|
226
|
+
{
|
|
227
|
+
serverInfo: MCP_SERVER_INFO,
|
|
228
|
+
transport: {
|
|
229
|
+
type: "streamable-http",
|
|
230
|
+
endpoint: `${origin}/mcp`,
|
|
231
|
+
},
|
|
232
|
+
capabilities: {
|
|
233
|
+
tools: {},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
200,
|
|
237
|
+
{
|
|
238
|
+
...discoveryHeaders,
|
|
239
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
app.get("/.well-known/agent-card.json", (c) => {
|
|
245
|
+
const origin = new URL(c.req.url).origin
|
|
246
|
+
|
|
247
|
+
return c.json(buildAgentCard(origin), 200, {
|
|
248
|
+
...discoveryHeaders,
|
|
249
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
app.get("/webmcp/manifest.json", (c) =>
|
|
254
|
+
c.json(buildWebMcpManifest(), 200, {
|
|
255
|
+
...discoveryHeaders,
|
|
256
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
|
|
149
260
|
app.get("/bot", (c) => c.redirect("/#bot", 302))
|
|
150
261
|
|
|
151
262
|
app.all("/mcp", async (c) => {
|
package/src/lib/a2a.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MCP_SERVER_INFO, TOOL_DEFINITIONS } from "./mcp"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The A2A protocol version each interface exposes, as `major.minor`.
|
|
5
|
+
* See https://a2a-protocol.org/latest/specification/
|
|
6
|
+
*/
|
|
7
|
+
const A2A_PROTOCOL_VERSION = "0.3"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The transport binding advertised for the agent's interface.
|
|
11
|
+
* Sosumi exposes its documentation service over plain HTTP requests,
|
|
12
|
+
* so the `HTTP+JSON` binding is the closest of A2A's officially supported bindings.
|
|
13
|
+
*/
|
|
14
|
+
const TRANSPORT = "HTTP+JSON"
|
|
15
|
+
|
|
16
|
+
const AGENT_DESCRIPTION =
|
|
17
|
+
"Making Apple docs AI-readable. " +
|
|
18
|
+
"Sosumi converts Apple Developer documentation, Human Interface Guidelines, " +
|
|
19
|
+
"WWDC session transcripts, and public Swift-DocC sites " +
|
|
20
|
+
"into clean Markdown for AI agents."
|
|
21
|
+
|
|
22
|
+
/** Keyword tags describing each skill's capabilities. */
|
|
23
|
+
const SKILL_TAGS: Record<string, string[]> = {
|
|
24
|
+
searchAppleDocumentation: ["apple", "search", "documentation"],
|
|
25
|
+
fetchAppleDocumentation: ["apple", "documentation", "markdown", "hig"],
|
|
26
|
+
fetchExternalDocumentation: ["swift-docc", "documentation", "markdown"],
|
|
27
|
+
fetchAppleVideoTranscript: ["apple", "wwdc", "video", "transcript"],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Example prompts illustrating how each skill is used. */
|
|
31
|
+
const SKILL_EXAMPLES: Record<string, string[]> = {
|
|
32
|
+
searchAppleDocumentation: ["Search Apple documentation for URLSession"],
|
|
33
|
+
fetchAppleDocumentation: ["Fetch /documentation/swiftui/view as Markdown"],
|
|
34
|
+
fetchExternalDocumentation: [
|
|
35
|
+
"Fetch https://apple.github.io/swift-argument-parser/documentation/argumentparser",
|
|
36
|
+
],
|
|
37
|
+
fetchAppleVideoTranscript: ["Fetch the transcript for /videos/play/wwdc2021/10133"],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface AgentInterface {
|
|
41
|
+
url: string
|
|
42
|
+
/** The A2A protocol version this interface exposes. */
|
|
43
|
+
protocolVersion: string
|
|
44
|
+
/** Transport binding. Named `protocolBinding` by the current A2A proto schema. */
|
|
45
|
+
protocolBinding: string
|
|
46
|
+
/** Alias of `protocolBinding` for clients reading the published JSON schema's `transport`. */
|
|
47
|
+
transport: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build an A2A Agent Card for the given origin.
|
|
52
|
+
* Conforms to the A2A Agent Card schema for agent-to-agent discovery.
|
|
53
|
+
* https://a2a-protocol.org/latest/topics/agent-discovery/
|
|
54
|
+
*/
|
|
55
|
+
export function buildAgentCard(origin: string) {
|
|
56
|
+
const service: AgentInterface = {
|
|
57
|
+
url: origin,
|
|
58
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
59
|
+
protocolBinding: TRANSPORT,
|
|
60
|
+
transport: TRANSPORT,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const skills = Object.values(TOOL_DEFINITIONS).map((def) => ({
|
|
64
|
+
// Convert the camelCase tool name to a kebab-case skill id.
|
|
65
|
+
id: def.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(),
|
|
66
|
+
name: def.title,
|
|
67
|
+
description: def.description,
|
|
68
|
+
tags: SKILL_TAGS[def.name] ?? ["apple", "documentation"],
|
|
69
|
+
examples: SKILL_EXAMPLES[def.name],
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
name: MCP_SERVER_INFO.name,
|
|
74
|
+
description: AGENT_DESCRIPTION,
|
|
75
|
+
version: MCP_SERVER_INFO.version,
|
|
76
|
+
supportedInterfaces: [service],
|
|
77
|
+
provider: {
|
|
78
|
+
organization: "NSHipster",
|
|
79
|
+
url: origin,
|
|
80
|
+
},
|
|
81
|
+
documentationUrl: `${origin}/SKILL.md`,
|
|
82
|
+
capabilities: {
|
|
83
|
+
streaming: false,
|
|
84
|
+
pushNotifications: false,
|
|
85
|
+
},
|
|
86
|
+
defaultInputModes: ["text/plain"],
|
|
87
|
+
defaultOutputModes: ["text/markdown", "text/plain"],
|
|
88
|
+
skills,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderFromJSON } from "../reference"
|
|
2
2
|
import type { AppleDocJSON } from "../types"
|
|
3
|
+
import { webBotAuthHeaders } from "../webbotauth"
|
|
3
4
|
import {
|
|
4
5
|
assertExternalDocumentationAccess,
|
|
5
6
|
ExternalAccessError,
|
|
@@ -42,6 +43,7 @@ export async function fetchExternalDocCJSON(
|
|
|
42
43
|
headers: {
|
|
43
44
|
"User-Agent": EXTERNAL_DOC_USER_AGENT,
|
|
44
45
|
Accept: "application/json",
|
|
46
|
+
...(await webBotAuthHeaders("GET", jsonUrl)),
|
|
45
47
|
},
|
|
46
48
|
})
|
|
47
49
|
|
|
@@ -88,6 +90,7 @@ export async function fetchRobotsPolicy(
|
|
|
88
90
|
headers: {
|
|
89
91
|
"User-Agent": userAgent,
|
|
90
92
|
Accept: "text/plain, text/*;q=0.9, */*;q=0.1",
|
|
93
|
+
...(await webBotAuthHeaders("GET", robotsUrl)),
|
|
91
94
|
},
|
|
92
95
|
})
|
|
93
96
|
|