sbox-api-mcp 1.1.0 → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbox-api",
3
- "description": "S&box game engine API documentation lookup for Claude Code. Search types, methods, properties, and documentation across the entire S&box API.",
3
+ "description": "S&box game engine MCP for Claude Code. Search API types, methods, and properties. Browse wiki documentation (guides, tutorials, concepts). Find Workshop community assets (models, sounds, maps, and more).",
4
4
  "author": {
5
5
  "name": "sofianebel"
6
6
  }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # sbox-api-mcp
2
2
 
3
- MCP server for [S&box](https://sbox.game) game engine API documentation lookup. Indexes **1800+ types**, **15,000+ members**, and **8,000+ documentation entries** from the S&box API, providing fast fuzzy search directly from Claude Code.
3
+ MCP server for [S&box](https://sbox.game) game engine API reference, wiki documentation, and Workshop assets. Indexes **1800+ types**, **15,000+ members**, and **8,000+ documentation entries** from the S&box API, plus **200+ wiki pages** (guides, tutorials, concepts) and community Workshop assets, all searchable directly from Claude Code.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,12 +26,19 @@ npx sbox-api-mcp
26
26
 
27
27
  | Tool | Description | Example |
28
28
  |------|-------------|---------|
29
+ | **API Reference** | | |
29
30
  | `search_types` | Fuzzy search types (classes, structs, enums, interfaces) | `search_types({ query: "GameObject" })` |
30
31
  | `get_type` | Full details of a specific type (methods, properties, fields) | `get_type({ name: "Sandbox.GameObject" })` |
31
32
  | `search_members` | Search methods/properties across all types | `search_members({ query: "Position", kind: "property" })` |
32
33
  | `list_namespaces` | List all API namespaces with type counts | `list_namespaces({ filter: "Audio" })` |
33
34
  | `search_docs` | Full-text search in documentation summaries | `search_docs({ query: "play sound" })` |
34
35
  | `update_api_source` | Update API data URL when S&box releases a new version | `update_api_source({ url: "https://cdn.sbox.game/releases/..." })` |
36
+ | **Wiki Documentation** | | |
37
+ | `search_wiki` | Search s&box wiki pages (guides, tutorials, concepts) | `search_wiki({ query: "networking" })` |
38
+ | `get_wiki_page` | Get the full content of a wiki page as markdown | `get_wiki_page({ path: "scene/components" })` |
39
+ | **Workshop (UGC)** | | |
40
+ | `search_packages` | Search community Workshop assets (models, sounds, maps, etc.) | `search_packages({ query: "zombie" })` |
41
+ | `get_package` | Get details of a specific Workshop package | `get_package({ ident: "facepunch.zombiemale" })` |
35
42
 
36
43
  ## Configuration
37
44
 
@@ -53,9 +60,11 @@ The URL format is: `https://cdn.sbox.game/releases/YYYY-MM-DD-HH-MM-SS.zip.json`
53
60
 
54
61
  ## How It Works
55
62
 
56
- On first startup, the server downloads the S&box API JSON (~9 MB) and caches it locally in `~/.sbox-api-mcp/`. Subsequent startups use an ETag check to skip re-downloading if the data hasn't changed.
63
+ **API Reference:** On first startup, the server downloads the S&box API JSON (~9 MB) and caches it locally in `~/.sbox-api-mcp/`. Subsequent startups use an ETag check to skip re-downloading if the data hasn't changed. The API data is indexed in-memory using [Fuse.js](https://www.fusejs.io/) for fast fuzzy search across type names, member names, and documentation summaries.
57
64
 
58
- The API data is indexed in-memory using [Fuse.js](https://www.fusejs.io/) for fast fuzzy search across type names, member names, and documentation summaries.
65
+ **Wiki Documentation:** Leverages S&box's LLM-optimized endpoints (`llms.txt` index + `.md` raw markdown pages). The wiki index is fetched on first use and cached in-memory for 1 hour. Individual pages are fetched live from `sbox.game/dev/doc/` as raw markdown.
66
+
67
+ **Workshop (UGC):** Queries the Facepunch services API live for community-made assets.
59
68
 
60
69
  ## License
61
70
 
@@ -0,0 +1,11 @@
1
+ export interface WikiEntry {
2
+ title: string;
3
+ path: string;
4
+ section: string;
5
+ }
6
+ export declare function searchWikiIndex(query: string, section?: string, limit?: number): Promise<WikiEntry[]>;
7
+ export declare function getWikiSections(): Promise<Map<string, number>>;
8
+ export declare function fetchWikiPage(path: string): Promise<{
9
+ content: string;
10
+ url: string;
11
+ }>;
@@ -0,0 +1,111 @@
1
+ import Fuse from 'fuse.js';
2
+ const LLMS_TXT_URL = 'https://sbox.game/llms.txt';
3
+ const WIKI_BASE_URL = 'https://sbox.game';
4
+ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
5
+ const SECTION_LABELS = {
6
+ 'getting-started': 'Getting Started',
7
+ 'scene': 'Scene',
8
+ 'code': 'Code',
9
+ 'editor': 'Editor',
10
+ 'assets': 'Assets',
11
+ 'graphics': 'Graphics',
12
+ 'ui': 'UI',
13
+ 'gameplay': 'Gameplay',
14
+ 'networking': 'Networking',
15
+ 'services': 'Services',
16
+ };
17
+ let cache = null;
18
+ function parseLlmsTxt(raw) {
19
+ const entries = [];
20
+ const linkPattern = /^-\s+\[(.+?)]\((.+?)\)/;
21
+ for (const line of raw.split('\n')) {
22
+ const match = line.match(linkPattern);
23
+ if (!match)
24
+ continue;
25
+ const [, title, path] = match;
26
+ if (!path.startsWith('/dev/doc/'))
27
+ continue;
28
+ // Extract section from first path segment after /dev/doc/
29
+ const segments = path.replace('/dev/doc/', '').replace(/\.md$/, '').split('/');
30
+ const sectionSlug = segments[0];
31
+ const section = SECTION_LABELS[sectionSlug] ?? sectionSlug;
32
+ entries.push({ title, path, section });
33
+ }
34
+ return entries;
35
+ }
36
+ function buildWikiFuse(entries) {
37
+ return new Fuse(entries, {
38
+ keys: [
39
+ { name: 'title', weight: 3 },
40
+ { name: 'path', weight: 1 },
41
+ { name: 'section', weight: 0.5 },
42
+ ],
43
+ threshold: 0.4,
44
+ includeScore: true,
45
+ minMatchCharLength: 2,
46
+ });
47
+ }
48
+ function buildSectionCounts(entries) {
49
+ const sections = new Map();
50
+ for (const entry of entries) {
51
+ sections.set(entry.section, (sections.get(entry.section) ?? 0) + 1);
52
+ }
53
+ return sections;
54
+ }
55
+ async function fetchLlmsTxt() {
56
+ const res = await fetch(LLMS_TXT_URL);
57
+ if (!res.ok) {
58
+ throw new Error(`Failed to fetch llms.txt: ${res.status} ${res.statusText}`);
59
+ }
60
+ return res.text();
61
+ }
62
+ async function getOrBuildIndex() {
63
+ if (cache && Date.now() < cache.expiry) {
64
+ return cache;
65
+ }
66
+ console.error('[sbox-api] Fetching wiki index from llms.txt...');
67
+ const raw = await fetchLlmsTxt();
68
+ const entries = parseLlmsTxt(raw);
69
+ const fuse = buildWikiFuse(entries);
70
+ const sections = buildSectionCounts(entries);
71
+ console.error(`[sbox-api] Wiki index: ${entries.length} pages, ${sections.size} sections`);
72
+ cache = { entries, fuse, sections, expiry: Date.now() + CACHE_TTL };
73
+ return cache;
74
+ }
75
+ export async function searchWikiIndex(query, section, limit = 10) {
76
+ const index = await getOrBuildIndex();
77
+ if (!query) {
78
+ let entries = index.entries;
79
+ if (section) {
80
+ entries = entries.filter(e => e.section.toLowerCase() === section.toLowerCase());
81
+ }
82
+ return entries.slice(0, limit);
83
+ }
84
+ const results = index.fuse.search(query, { limit: limit * 2 });
85
+ let entries = results.map(r => r.item);
86
+ if (section) {
87
+ entries = entries.filter(e => e.section.toLowerCase() === section.toLowerCase());
88
+ }
89
+ return entries.slice(0, limit);
90
+ }
91
+ export async function getWikiSections() {
92
+ const index = await getOrBuildIndex();
93
+ return index.sections;
94
+ }
95
+ export async function fetchWikiPage(path) {
96
+ // Normalize path: accept various formats
97
+ let normalized = path.trim();
98
+ // Strip leading /dev/doc/ if present
99
+ normalized = normalized.replace(/^\/dev\/doc\//, '');
100
+ // Strip leading slash
101
+ normalized = normalized.replace(/^\//, '');
102
+ // Strip trailing .md
103
+ normalized = normalized.replace(/\.md$/, '');
104
+ const url = `${WIKI_BASE_URL}/dev/doc/${normalized}.md`;
105
+ const res = await fetch(url);
106
+ if (!res.ok) {
107
+ throw new Error(`Wiki page not found: ${res.status} ${res.statusText} (${url})`);
108
+ }
109
+ const content = await res.text();
110
+ return { content, url };
111
+ }
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { registerSearchDocs } from './tools/search-docs.js';
11
11
  import { registerUpdateSource } from './tools/update-source.js';
12
12
  import { registerSearchPackages } from './tools/search-packages.js';
13
13
  import { registerGetPackage } from './tools/get-package.js';
14
+ import { registerSearchWiki } from './tools/search-wiki.js';
15
+ import { registerGetWikiPage } from './tools/get-wiki-page.js';
14
16
  async function main() {
15
17
  console.error('[sbox-api] Starting S&box API MCP Server...');
16
18
  // Load API data
@@ -31,6 +33,9 @@ async function main() {
31
33
  // UGC Workshop tools (no indexes needed - live API)
32
34
  registerSearchPackages(server);
33
35
  registerGetPackage(server);
36
+ // Wiki documentation tools (live API, in-memory cached index)
37
+ registerSearchWiki(server);
38
+ registerGetWikiPage(server);
34
39
  // Connect via stdio
35
40
  const transport = new StdioServerTransport();
36
41
  await server.connect(transport);
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGetWikiPage(server: McpServer): void;
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { fetchWikiPage } from '../data/wiki-client.js';
3
+ export function registerGetWikiPage(server) {
4
+ server.registerTool('get_wiki_page', {
5
+ title: 'Get S&box Wiki Page',
6
+ description: 'Fetch a specific s&box wiki documentation page as raw markdown. Use search_wiki to find page paths, or provide a known path directly.',
7
+ inputSchema: {
8
+ path: z
9
+ .string()
10
+ .describe("Wiki page path (e.g., 'scene/components', 'networking/rpc-messages', 'getting-started/first-project')"),
11
+ },
12
+ }, async ({ path }) => {
13
+ try {
14
+ const { content, url } = await fetchWikiPage(path);
15
+ return {
16
+ content: [{
17
+ type: 'text',
18
+ text: `Source: ${url}\n\n---\n\n${content}`,
19
+ }],
20
+ };
21
+ }
22
+ catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: `Error fetching wiki page: ${message}\n\nTip: Use search_wiki to find valid page paths.`,
28
+ }],
29
+ };
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSearchWiki(server: McpServer): void;
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ import { searchWikiIndex, getWikiSections } from '../data/wiki-client.js';
3
+ function formatEntry(entry, index) {
4
+ const slug = entry.path.replace('/dev/doc/', '').replace(/\.md$/, '');
5
+ return `${index + 1}. **${entry.title}** [${entry.section}]\n Path: ${slug}`;
6
+ }
7
+ export function registerSearchWiki(server) {
8
+ server.registerTool('search_wiki', {
9
+ title: 'Search S&box Wiki Documentation',
10
+ description: 'Search the s&box wiki documentation for guides, tutorials, and conceptual docs. Returns matching page titles with paths. Use get_wiki_page with a path to read a specific page.',
11
+ inputSchema: {
12
+ query: z
13
+ .string()
14
+ .default('')
15
+ .describe("Topic to search for (e.g., 'networking', 'components', 'shader graph'). Leave empty to list sections or browse."),
16
+ section: z
17
+ .string()
18
+ .optional()
19
+ .describe("Filter by documentation section (e.g., 'Scene', 'Networking', 'Graphics', 'UI', 'Editor', 'Gameplay', 'Getting Started')"),
20
+ limit: z.number().min(1).max(50).default(10).describe('Max results (1-50, default 10)'),
21
+ },
22
+ }, async ({ query, section, limit }) => {
23
+ try {
24
+ // No query and no section: show sections overview
25
+ if (!query && !section) {
26
+ const sections = await getWikiSections();
27
+ const lines = Array.from(sections.entries())
28
+ .sort(([, a], [, b]) => b - a)
29
+ .map(([name, count]) => `- **${name}** (${count} pages)`);
30
+ const total = Array.from(sections.values()).reduce((a, b) => a + b, 0);
31
+ return {
32
+ content: [{
33
+ type: 'text',
34
+ text: `S&box Wiki Documentation — ${total} pages in ${sections.size} sections:\n\n${lines.join('\n')}\n\nTip: Use search_wiki with a query or section to find specific pages.`,
35
+ }],
36
+ };
37
+ }
38
+ const results = await searchWikiIndex(query, section, limit);
39
+ if (results.length === 0) {
40
+ const hint = section ? ` in section "${section}"` : '';
41
+ return {
42
+ content: [{
43
+ type: 'text',
44
+ text: `No wiki pages found matching "${query}"${hint}. Try broadening your search or removing the section filter.`,
45
+ }],
46
+ };
47
+ }
48
+ const formatted = results.map((entry, i) => formatEntry(entry, i));
49
+ const sectionLabel = section ? ` in "${section}"` : '';
50
+ const queryLabel = query ? `matching "${query}"` : '';
51
+ const header = `Found ${results.length} wiki pages ${queryLabel}${sectionLabel}:\n`;
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: `${header}\n${formatted.join('\n\n')}\n\nTip: Use get_wiki_page with the path to read a page.`,
56
+ }],
57
+ };
58
+ }
59
+ catch (err) {
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: `Error searching wiki: ${err instanceof Error ? err.message : String(err)}`,
64
+ }],
65
+ };
66
+ }
67
+ });
68
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sbox-api-mcp",
3
- "version": "1.1.0",
4
- "description": "MCP server for S&box game engine API documentation and Workshop (UGC) asset search. Search types, methods, properties, documentation, and community-made models, sounds, maps, and more.",
3
+ "version": "1.2.0",
4
+ "description": "MCP server for S&box game engine API reference, wiki documentation, and Workshop (UGC) asset search. Search types, methods, properties, wiki guides/tutorials, and community-made models, sounds, maps, and more.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sbox-api-mcp": "dist/index.js"
@@ -26,6 +26,7 @@
26
26
  "documentation",
27
27
  "claude",
28
28
  "claude-code",
29
+ "wiki",
29
30
  "ugc",
30
31
  "workshop",
31
32
  "assets",