Skip to content

Commit 988a49f

Browse files
committed
feat(api): add interactive Swagger UI docs at /docs
- Add OpenAPI 3.1 spec with all endpoints, schemas, and tags - Serve Swagger UI at GET /docs with try-it-out enabled - Serve OpenAPI JSON at GET /openapi.json - Add API info endpoint at GET / with endpoint listing - Update serve command to show /docs and /openapi.json endpoints - Update REST API docs page with interactive docs section
1 parent 19694dc commit 988a49f

File tree

4 files changed

+308
-0
lines changed

4 files changed

+308
-0
lines changed

docs/fumadocs/content/docs/rest-api.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,16 @@ Search results are ranked using a multi-signal scoring system (0-100):
176176
| Popularity | 15 | Log-scaled stars + installs |
177177
| References | 15 | Has example files or docs |
178178

179+
## Interactive API Documentation
180+
181+
The server includes built-in interactive API docs powered by Swagger UI:
182+
183+
- **`GET /docs`** — Interactive Swagger UI with try-it-out support
184+
- **`GET /openapi.json`** — OpenAPI 3.1 specification
185+
- **`GET /`** — API info with endpoint listing
186+
187+
Open `http://localhost:3737/docs` in your browser after starting the server.
188+
179189
## Caching
180190

181191
The server uses an in-memory LRU cache with configurable TTL. Search results are cached by query parameters to avoid redundant ranking computations.

packages/api/src/routes/docs.ts

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { Hono } from 'hono';
2+
3+
const OPENAPI_SPEC = {
4+
openapi: '3.1.0',
5+
info: {
6+
title: 'SkillKit',
7+
version: '1.12.0',
8+
description: 'Skill Discovery API for AI coding agents. Search, browse, and retrieve skills from the SkillKit marketplace (15,000+ skills across 32 agents).',
9+
license: { name: 'Apache-2.0', url: 'https://opensource.org/licenses/Apache-2.0' },
10+
contact: { name: 'SkillKit', url: 'https://github.com/rohitg00/skillkit' },
11+
},
12+
servers: [
13+
{ url: 'http://localhost:3737', description: 'Local development' },
14+
],
15+
paths: {
16+
'/search': {
17+
get: {
18+
summary: 'Search skills',
19+
description: 'Locate agent skills matching a query, ranked by multi-signal relevance (content, query match, popularity, references).',
20+
operationId: 'searchSkillsGet',
21+
tags: ['Search'],
22+
parameters: [
23+
{ name: 'q', in: 'query', required: true, schema: { type: 'string', minLength: 1, maxLength: 200 }, description: 'Search query' },
24+
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20, minimum: 1, maximum: 100 }, description: 'Max results to return' },
25+
{ name: 'include_content', in: 'query', schema: { type: 'boolean', default: false }, description: 'Include full skill content in response' },
26+
],
27+
responses: {
28+
'200': { description: 'Search results', content: { 'application/json': { schema: { $ref: '#/components/schemas/SearchResponse' } } } },
29+
'400': { description: 'Missing query parameter', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
30+
'429': { description: 'Rate limit exceeded', content: { 'application/json': { schema: { $ref: '#/components/schemas/RateLimitResponse' } } } },
31+
},
32+
},
33+
post: {
34+
summary: 'Search skills with filters',
35+
description: 'Search with advanced filters including tags, category, and source.',
36+
operationId: 'searchSkillsPost',
37+
tags: ['Search'],
38+
requestBody: {
39+
required: true,
40+
content: {
41+
'application/json': {
42+
schema: {
43+
type: 'object',
44+
required: ['query'],
45+
properties: {
46+
query: { type: 'string', description: 'Search query' },
47+
limit: { type: 'integer', default: 20, minimum: 1, maximum: 100, description: 'Max results' },
48+
include_content: { type: 'boolean', default: false, description: 'Include full content' },
49+
filters: {
50+
type: 'object',
51+
properties: {
52+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
53+
category: { type: 'string', description: 'Filter by category' },
54+
source: { type: 'string', description: 'Filter by source repo' },
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
responses: {
63+
'200': { description: 'Filtered search results', content: { 'application/json': { schema: { $ref: '#/components/schemas/SearchResponse' } } } },
64+
'400': { description: 'Invalid request', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
65+
},
66+
},
67+
},
68+
'/skills/{owner}/{repo}/{id}': {
69+
get: {
70+
summary: 'Get skill by ID',
71+
description: 'Retrieve a specific skill by its source repository and name.',
72+
operationId: 'getSkill',
73+
tags: ['Skills'],
74+
parameters: [
75+
{ name: 'owner', in: 'path', required: true, schema: { type: 'string' }, description: 'Repository owner' },
76+
{ name: 'repo', in: 'path', required: true, schema: { type: 'string' }, description: 'Repository name' },
77+
{ name: 'id', in: 'path', required: true, schema: { type: 'string' }, description: 'Skill name' },
78+
],
79+
responses: {
80+
'200': { description: 'Skill details', content: { 'application/json': { schema: { $ref: '#/components/schemas/Skill' } } } },
81+
'404': { description: 'Skill not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } },
82+
},
83+
},
84+
},
85+
'/trending': {
86+
get: {
87+
summary: 'Trending skills',
88+
description: 'Get top skills ranked by multi-signal relevance score (content availability, popularity, references).',
89+
operationId: 'getTrending',
90+
tags: ['Discovery'],
91+
parameters: [
92+
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20, minimum: 1, maximum: 100 }, description: 'Max results' },
93+
],
94+
responses: {
95+
'200': { description: 'Trending skills', content: { 'application/json': { schema: { $ref: '#/components/schemas/TrendingResponse' } } } },
96+
},
97+
},
98+
},
99+
'/categories': {
100+
get: {
101+
summary: 'Skill categories',
102+
description: 'List all skill categories and tags with their counts, sorted by popularity.',
103+
operationId: 'getCategories',
104+
tags: ['Discovery'],
105+
responses: {
106+
'200': { description: 'Category list', content: { 'application/json': { schema: { $ref: '#/components/schemas/CategoriesResponse' } } } },
107+
},
108+
},
109+
},
110+
'/health': {
111+
get: {
112+
summary: 'Health check',
113+
description: 'Server health status including version, skill count, and uptime.',
114+
operationId: 'getHealth',
115+
tags: ['System'],
116+
responses: {
117+
'200': { description: 'Server health', content: { 'application/json': { schema: { $ref: '#/components/schemas/HealthResponse' } } } },
118+
},
119+
},
120+
},
121+
'/cache/stats': {
122+
get: {
123+
summary: 'Cache statistics',
124+
description: 'Cache hit/miss rates and current size.',
125+
operationId: 'getCacheStats',
126+
tags: ['System'],
127+
responses: {
128+
'200': { description: 'Cache stats', content: { 'application/json': { schema: { $ref: '#/components/schemas/CacheStatsResponse' } } } },
129+
},
130+
},
131+
},
132+
},
133+
components: {
134+
schemas: {
135+
Skill: {
136+
type: 'object',
137+
properties: {
138+
name: { type: 'string', description: 'Skill name' },
139+
description: { type: 'string', description: 'Skill description' },
140+
source: { type: 'string', description: 'Source repository (owner/repo)' },
141+
repo: { type: 'string', description: 'Repository URL' },
142+
tags: { type: 'array', items: { type: 'string' }, description: 'Skill tags' },
143+
category: { type: 'string', description: 'Skill category' },
144+
content: { type: 'string', description: 'Full SKILL.md content (when requested)' },
145+
stars: { type: 'integer', description: 'GitHub stars' },
146+
installs: { type: 'integer', description: 'Install count' },
147+
},
148+
required: ['name', 'source'],
149+
},
150+
SearchResponse: {
151+
type: 'object',
152+
properties: {
153+
skills: { type: 'array', items: { $ref: '#/components/schemas/Skill' } },
154+
total: { type: 'integer', description: 'Total matching skills' },
155+
query: { type: 'string', description: 'Original query' },
156+
limit: { type: 'integer', description: 'Applied limit' },
157+
},
158+
required: ['skills', 'total', 'query', 'limit'],
159+
},
160+
TrendingResponse: {
161+
type: 'object',
162+
properties: {
163+
skills: { type: 'array', items: { $ref: '#/components/schemas/Skill' } },
164+
limit: { type: 'integer' },
165+
},
166+
required: ['skills', 'limit'],
167+
},
168+
CategoriesResponse: {
169+
type: 'object',
170+
properties: {
171+
categories: { type: 'array', items: { $ref: '#/components/schemas/CategoryCount' } },
172+
total: { type: 'integer' },
173+
},
174+
required: ['categories', 'total'],
175+
},
176+
CategoryCount: {
177+
type: 'object',
178+
properties: {
179+
name: { type: 'string' },
180+
count: { type: 'integer' },
181+
},
182+
required: ['name', 'count'],
183+
},
184+
HealthResponse: {
185+
type: 'object',
186+
properties: {
187+
status: { type: 'string', enum: ['ok'] },
188+
version: { type: 'string' },
189+
skillCount: { type: 'integer' },
190+
uptime: { type: 'number', description: 'Uptime in seconds' },
191+
},
192+
required: ['status', 'version', 'skillCount', 'uptime'],
193+
},
194+
CacheStatsResponse: {
195+
type: 'object',
196+
properties: {
197+
hits: { type: 'integer' },
198+
misses: { type: 'integer' },
199+
size: { type: 'integer' },
200+
maxSize: { type: 'integer' },
201+
hitRate: { type: 'number' },
202+
},
203+
required: ['hits', 'misses', 'size', 'maxSize', 'hitRate'],
204+
},
205+
ErrorResponse: {
206+
type: 'object',
207+
properties: {
208+
error: { type: 'string' },
209+
},
210+
required: ['error'],
211+
},
212+
RateLimitResponse: {
213+
type: 'object',
214+
properties: {
215+
error: { type: 'string' },
216+
retryAfter: { type: 'integer', description: 'Seconds until rate limit resets' },
217+
},
218+
required: ['error', 'retryAfter'],
219+
},
220+
},
221+
},
222+
tags: [
223+
{ name: 'Search', description: 'Skill search and filtering' },
224+
{ name: 'Skills', description: 'Individual skill retrieval' },
225+
{ name: 'Discovery', description: 'Trending skills and categories' },
226+
{ name: 'System', description: 'Health checks and diagnostics' },
227+
],
228+
};
229+
230+
const SWAGGER_HTML = `<!DOCTYPE html>
231+
<html lang="en">
232+
<head>
233+
<meta charset="UTF-8">
234+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
235+
<title>SkillKit API Documentation</title>
236+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
237+
<style>
238+
body { margin: 0; background: #1a1a2e; }
239+
.swagger-ui .topbar { display: none; }
240+
.swagger-ui { max-width: 1200px; margin: 0 auto; }
241+
.swagger-ui .info .title { font-family: monospace; }
242+
</style>
243+
</head>
244+
<body>
245+
<div id="swagger-ui"></div>
246+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
247+
<script>
248+
SwaggerUIBundle({
249+
url: '/openapi.json',
250+
dom_id: '#swagger-ui',
251+
deepLinking: true,
252+
showExtensions: true,
253+
showCommonExtensions: true,
254+
defaultModelsExpandDepth: 2,
255+
defaultModelExpandDepth: 2,
256+
docExpansion: 'list',
257+
filter: true,
258+
tryItOutEnabled: true,
259+
});
260+
</script>
261+
</body>
262+
</html>`;
263+
264+
export function docsRoutes() {
265+
const app = new Hono();
266+
267+
app.get('/openapi.json', (c) => {
268+
return c.json(OPENAPI_SPEC);
269+
});
270+
271+
app.get('/docs', (c) => {
272+
return c.html(SWAGGER_HTML);
273+
});
274+
275+
app.get('/', (c) => {
276+
return c.json({
277+
name: 'SkillKit API',
278+
version: '1.12.0',
279+
docs: '/docs',
280+
openapi: '/openapi.json',
281+
endpoints: {
282+
search: 'GET /search?q=...',
283+
searchFiltered: 'POST /search',
284+
skill: 'GET /skills/:owner/:repo/:id',
285+
trending: 'GET /trending',
286+
categories: 'GET /categories',
287+
health: 'GET /health',
288+
cache: 'GET /cache/stats',
289+
},
290+
});
291+
});
292+
293+
return app;
294+
}

packages/api/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { searchRoutes } from './routes/search.js';
77
import { skillRoutes } from './routes/skills.js';
88
import { trendingRoutes } from './routes/trending.js';
99
import { categoryRoutes } from './routes/categories.js';
10+
import { docsRoutes } from './routes/docs.js';
1011
import type { ApiSkill, SearchResponse } from './types.js';
1112

1213
export interface ServerOptions {
@@ -35,6 +36,7 @@ export function createApp(options: ServerOptions = {}) {
3536
app.route('/', skillRoutes(skills));
3637
app.route('/', trendingRoutes(skills));
3738
app.route('/', categoryRoutes(skills));
39+
app.route('/', docsRoutes());
3840

3941
return { app, cache };
4042
}

packages/cli/src/commands/serve.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export class ServeCommand extends Command {
7878
console.log(colors.muted(` GET /trending - Top skills`));
7979
console.log(colors.muted(` GET /categories - Skill categories`));
8080
console.log(colors.muted(` GET /cache/stats - Cache statistics`));
81+
console.log(colors.muted(` GET /docs - Interactive API docs`));
82+
console.log(colors.muted(` GET /openapi.json - OpenAPI specification`));
8183
console.log('');
8284
console.log(colors.muted('Press Ctrl+C to stop'));
8385

0 commit comments

Comments
 (0)