@nshipster/sosumi 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.md +19 -0
- package/README.md +304 -0
- package/bin/sosumi.mjs +76 -0
- package/package.json +53 -0
- package/public/_headers +2 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +7 -0
- package/public/icons/square.and.pencil.svg +15 -0
- package/public/index.html +898 -0
- package/public/llms.txt +184 -0
- package/public/sosumi.m4a +0 -0
- package/src/cli.ts +214 -0
- package/src/index.ts +507 -0
- package/src/lib/cli-endpoints.ts +106 -0
- package/src/lib/external/fetch.ts +133 -0
- package/src/lib/external/index.ts +8 -0
- package/src/lib/external/policy.ts +308 -0
- package/src/lib/external/types.ts +10 -0
- package/src/lib/fetch.ts +43 -0
- package/src/lib/hig/fetch.ts +186 -0
- package/src/lib/hig/index.ts +9 -0
- package/src/lib/hig/render.ts +514 -0
- package/src/lib/hig/types.ts +206 -0
- package/src/lib/hig/util.ts +30 -0
- package/src/lib/mcp.ts +315 -0
- package/src/lib/reference/fetch.ts +53 -0
- package/src/lib/reference/index.ts +8 -0
- package/src/lib/reference/render.ts +739 -0
- package/src/lib/reference/types.ts +31 -0
- package/src/lib/search.ts +221 -0
- package/src/lib/types.ts +334 -0
- package/src/lib/url.ts +55 -0
- package/src/lib/video/index.ts +179 -0
- package/wrangler.jsonc +27 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HIGExternalReference, HIGImageReference, HIGReference, HIGTocItem } from "./types"
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// TYPE GUARDS
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Type guard to check if a reference is an image reference
|
|
9
|
+
*/
|
|
10
|
+
export function isHIGImageReference(
|
|
11
|
+
ref: HIGReference | HIGImageReference | HIGExternalReference,
|
|
12
|
+
): ref is HIGImageReference {
|
|
13
|
+
return "alt" in ref && "variants" in ref
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type guard to check if a reference is a topic reference
|
|
18
|
+
*/
|
|
19
|
+
export function isHIGTopicReference(
|
|
20
|
+
ref: HIGReference | HIGImageReference | HIGExternalReference,
|
|
21
|
+
): ref is HIGReference {
|
|
22
|
+
return ref.type === "topic"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type guard to check if a ToC item has children
|
|
27
|
+
*/
|
|
28
|
+
export function hasChildren(item: HIGTocItem): boolean {
|
|
29
|
+
return item.children !== undefined && item.children.length > 0
|
|
30
|
+
}
|
package/src/lib/mcp.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
import type { ExternalPolicyEnv } from "./external"
|
|
5
|
+
import { fetchExternalDocumentationMarkdown } from "./external"
|
|
6
|
+
import { fetchHIGPageData, renderHIGFromJSON } from "./hig"
|
|
7
|
+
import { fetchJSONData, renderFromJSON } from "./reference"
|
|
8
|
+
import { searchAppleDeveloperDocs } from "./search"
|
|
9
|
+
import { generateAppleDocUrl, normalizeDocumentationPath } from "./url"
|
|
10
|
+
import { fetchVideoTranscriptMarkdown } from "./video"
|
|
11
|
+
|
|
12
|
+
export function createMcpServer(externalPolicyEnv: ExternalPolicyEnv = {}) {
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "sosumi.ai",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Register Apple search tool
|
|
19
|
+
server.registerTool(
|
|
20
|
+
"searchAppleDocumentation",
|
|
21
|
+
{
|
|
22
|
+
title: "Search Apple Documentation",
|
|
23
|
+
description: "Search Apple Developer documentation and return structured results",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
query: z.string().describe("Search query for Apple documentation"),
|
|
26
|
+
},
|
|
27
|
+
outputSchema: {
|
|
28
|
+
query: z.string().describe("The search query that was executed"),
|
|
29
|
+
results: z
|
|
30
|
+
.array(
|
|
31
|
+
z.object({
|
|
32
|
+
title: z.string().describe("Title of the documentation page"),
|
|
33
|
+
url: z.string().describe("Full URL to the documentation page"),
|
|
34
|
+
description: z.string().describe("Brief description of the page content"),
|
|
35
|
+
breadcrumbs: z
|
|
36
|
+
.array(z.string())
|
|
37
|
+
.describe("Navigation breadcrumbs showing the page hierarchy"),
|
|
38
|
+
tags: z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.describe("Tags associated with the page (languages, platforms, etc.)"),
|
|
41
|
+
type: z.string().describe("Type of result (documentation, general, etc.)"),
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
.describe("Array of search results"),
|
|
45
|
+
},
|
|
46
|
+
annotations: {
|
|
47
|
+
readOnlyHint: true,
|
|
48
|
+
destructiveHint: false,
|
|
49
|
+
idempotentHint: true,
|
|
50
|
+
openWorldHint: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
async ({ query }) => {
|
|
54
|
+
try {
|
|
55
|
+
const searchResponse = await searchAppleDeveloperDocs(query)
|
|
56
|
+
|
|
57
|
+
const structuredContent = {
|
|
58
|
+
query: searchResponse.query,
|
|
59
|
+
results: searchResponse.results.map((result) => ({
|
|
60
|
+
title: result.title,
|
|
61
|
+
url: result.url,
|
|
62
|
+
description: result.description,
|
|
63
|
+
breadcrumbs: result.breadcrumbs,
|
|
64
|
+
tags: result.tags,
|
|
65
|
+
type: result.type,
|
|
66
|
+
})),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (searchResponse.results.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text" as const,
|
|
74
|
+
text: `No results found for "${query}"`,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
structuredContent,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Provide a readable text summary
|
|
82
|
+
const resultText =
|
|
83
|
+
`Found ${searchResponse.results.length} result(s) for "${query}":\n\n` +
|
|
84
|
+
searchResponse.results
|
|
85
|
+
.map(
|
|
86
|
+
(result, index) =>
|
|
87
|
+
`${index + 1}. ${result.title}\n ${result.url}\n ${result.description || "No description"}`,
|
|
88
|
+
)
|
|
89
|
+
.join("\n\n")
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text" as const,
|
|
95
|
+
text: resultText,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
structuredContent,
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
|
102
|
+
|
|
103
|
+
const structuredContent = {
|
|
104
|
+
query,
|
|
105
|
+
results: [],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text" as const,
|
|
112
|
+
text: `Error searching Apple Developer documentation: ${errorMessage}`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
structuredContent,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// Register documentation fetch tool (supports both dev docs and HIG)
|
|
122
|
+
server.registerTool(
|
|
123
|
+
"fetchAppleDocumentation",
|
|
124
|
+
{
|
|
125
|
+
title: "Fetch Apple Documentation",
|
|
126
|
+
description:
|
|
127
|
+
"Fetch Apple Developer documentation and Human Interface Guidelines by path and return as markdown",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
path: z
|
|
130
|
+
.string()
|
|
131
|
+
.describe(
|
|
132
|
+
"Documentation path (e.g., '/documentation/swift', 'swiftui/view', 'design/human-interface-guidelines/foundations/color')",
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
annotations: {
|
|
136
|
+
readOnlyHint: true,
|
|
137
|
+
destructiveHint: false,
|
|
138
|
+
idempotentHint: true,
|
|
139
|
+
openWorldHint: true,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
async ({ path }) => {
|
|
143
|
+
try {
|
|
144
|
+
// Check if this is a HIG path
|
|
145
|
+
if (path.includes("design/human-interface-guidelines")) {
|
|
146
|
+
// Handle HIG content
|
|
147
|
+
const higPath = path.replace(/^\/?(design\/human-interface-guidelines\/)/, "")
|
|
148
|
+
const sourceUrl = `https://developer.apple.com/design/human-interface-guidelines/${higPath}`
|
|
149
|
+
|
|
150
|
+
const jsonData = await fetchHIGPageData(higPath)
|
|
151
|
+
const markdown = await renderHIGFromJSON(jsonData, sourceUrl)
|
|
152
|
+
|
|
153
|
+
if (!markdown || markdown.trim().length < 100) {
|
|
154
|
+
throw new Error("Insufficient content in HIG page")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text" as const,
|
|
161
|
+
text: markdown,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Handle regular developer documentation
|
|
167
|
+
const normalizedPath = normalizeDocumentationPath(path)
|
|
168
|
+
const appleUrl = generateAppleDocUrl(normalizedPath)
|
|
169
|
+
|
|
170
|
+
const jsonData = await fetchJSONData(normalizedPath)
|
|
171
|
+
const markdown = await renderFromJSON(jsonData, appleUrl)
|
|
172
|
+
|
|
173
|
+
if (!markdown || markdown.trim().length < 100) {
|
|
174
|
+
throw new Error("Insufficient content in documentation")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: "text" as const,
|
|
181
|
+
text: markdown,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "text" as const,
|
|
193
|
+
text: `Error fetching content for "${path}": ${errorMessage}`,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Register external documentation fetch tool
|
|
202
|
+
server.registerTool(
|
|
203
|
+
"fetchExternalDocumentation",
|
|
204
|
+
{
|
|
205
|
+
title: "Fetch External Documentation",
|
|
206
|
+
description:
|
|
207
|
+
"Fetch external Swift-DocC documentation by absolute https URL and return as markdown",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
url: z
|
|
210
|
+
.string()
|
|
211
|
+
.describe(
|
|
212
|
+
"External Swift-DocC URL (e.g., 'https://apple.github.io/swift-argument-parser/documentation/argumentparser')",
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
annotations: {
|
|
216
|
+
readOnlyHint: true,
|
|
217
|
+
destructiveHint: false,
|
|
218
|
+
idempotentHint: true,
|
|
219
|
+
openWorldHint: true,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
async ({ url }) => {
|
|
223
|
+
try {
|
|
224
|
+
const markdown = await fetchExternalDocumentationMarkdown(url, externalPolicyEnv)
|
|
225
|
+
|
|
226
|
+
if (!markdown || markdown.trim().length < 100) {
|
|
227
|
+
throw new Error("Insufficient content in external documentation")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text" as const,
|
|
234
|
+
text: markdown,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text" as const,
|
|
245
|
+
text: `Error fetching external content for "${url}": ${errorMessage}`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
// Register Apple video transcript fetch tool
|
|
254
|
+
server.registerTool(
|
|
255
|
+
"fetchAppleVideoTranscript",
|
|
256
|
+
{
|
|
257
|
+
title: "Fetch Apple Video Transcript",
|
|
258
|
+
description: "Fetch transcript for an Apple Developer video path and return as markdown",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
path: z
|
|
261
|
+
.string()
|
|
262
|
+
.describe(
|
|
263
|
+
"Apple video path (e.g., '/videos/play/wwdc2021/10133' or '/videos/play/meet-with-apple/208')",
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
annotations: {
|
|
267
|
+
readOnlyHint: true,
|
|
268
|
+
destructiveHint: false,
|
|
269
|
+
idempotentHint: true,
|
|
270
|
+
openWorldHint: true,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
async ({ path }) => {
|
|
274
|
+
try {
|
|
275
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`
|
|
276
|
+
const match = normalizedPath.match(/^\/videos\/play\/([a-z0-9-]+)\/(\d+)\/?$/i)
|
|
277
|
+
if (!match) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"Invalid Apple video path. Expected format: /videos/play/COLLECTION/VIDEO_ID",
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const collection = match[1]
|
|
284
|
+
const videoId = match[2]
|
|
285
|
+
const sourceUrl = `https://developer.apple.com/videos/play/${collection}/${videoId}/`
|
|
286
|
+
const markdown = await fetchVideoTranscriptMarkdown(sourceUrl, collection, videoId)
|
|
287
|
+
|
|
288
|
+
if (!markdown || markdown.trim().length < 100) {
|
|
289
|
+
throw new Error("Insufficient content in video transcript")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text" as const,
|
|
296
|
+
text: markdown,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text" as const,
|
|
306
|
+
text: `Error fetching Apple video transcript for "${path}": ${errorMessage}`,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return server
|
|
315
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Developer Reference documentation fetching functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getRandomUserAgent, NotFoundError } from "../fetch"
|
|
6
|
+
import { normalizeDocumentationPath } from "../url"
|
|
7
|
+
import type { AppleDocJSON } from "./types"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch Apple Developer reference documentation JSON data for a given path
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchJSONData(path: string): Promise<AppleDocJSON> {
|
|
13
|
+
// Normalize the path using the shared function
|
|
14
|
+
const normalizedPath = normalizeDocumentationPath(path)
|
|
15
|
+
|
|
16
|
+
// Add back the documentation/ prefix for the JSON API
|
|
17
|
+
const jsonPath = `documentation/${normalizedPath}`
|
|
18
|
+
|
|
19
|
+
// Split path into parts
|
|
20
|
+
const parts = jsonPath.split("/")
|
|
21
|
+
|
|
22
|
+
let jsonUrl: string
|
|
23
|
+
if (parts.length === 2) {
|
|
24
|
+
// Top-level framework index (e.g., /documentation/swiftui)
|
|
25
|
+
const framework = parts[1]
|
|
26
|
+
jsonUrl = `https://developer.apple.com/tutorials/data/index/${framework}`
|
|
27
|
+
} else {
|
|
28
|
+
// Individual page (e.g., /documentation/swiftui/view)
|
|
29
|
+
jsonUrl = `https://developer.apple.com/tutorials/data/${jsonPath}.json`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Generate a random Safari user agent with uniform selection
|
|
33
|
+
const userAgent = getRandomUserAgent()
|
|
34
|
+
|
|
35
|
+
const response = await fetch(jsonUrl, {
|
|
36
|
+
headers: {
|
|
37
|
+
"User-Agent": userAgent,
|
|
38
|
+
Accept: "application/json",
|
|
39
|
+
"Cache-Control": "no-cache",
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
console.error(`Failed to fetch JSON: ${response.status} ${response.statusText}`)
|
|
45
|
+
if (response.status === 404) {
|
|
46
|
+
throw new NotFoundError(`Apple documentation page not found at ${jsonUrl}`)
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Failed to fetch JSON: ${response.status} ${response.statusText}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = (await response.json()) as AppleDocJSON
|
|
52
|
+
return data
|
|
53
|
+
}
|