sbox-api-mcp 1.0.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,53 @@
1
+ export declare const PACKAGE_TYPES: readonly ["model", "sound", "material", "map", "game", "clothing", "library", "vsnd", "prefab", "shader"];
2
+ export type PackageType = (typeof PACKAGE_TYPES)[number];
3
+ export interface PackageOrg {
4
+ Ident: string;
5
+ Title: string;
6
+ Description?: string;
7
+ Thumb?: string;
8
+ Twitter?: string;
9
+ WebUrl?: string;
10
+ Discord?: string;
11
+ }
12
+ export interface UgcPackage {
13
+ Org: PackageOrg;
14
+ Ident: string;
15
+ FullIdent: string;
16
+ Title: string;
17
+ Summary?: string;
18
+ Thumb?: string;
19
+ ThumbWide?: string;
20
+ ThumbTall?: string;
21
+ VideoThumb?: string;
22
+ TypeName: string;
23
+ Updated: string;
24
+ Created: string;
25
+ UsageStats?: Record<string, unknown>;
26
+ Tags?: string[];
27
+ Favourited?: number;
28
+ VotesUp?: number;
29
+ Collections?: number;
30
+ Referenced?: number;
31
+ Public?: boolean;
32
+ }
33
+ export interface SearchPackagesResponse {
34
+ Packages: UgcPackage[];
35
+ TotalCount: number;
36
+ Facets: unknown[];
37
+ Tags?: Record<string, number>;
38
+ Orders?: Array<{
39
+ Name: string;
40
+ Title: string;
41
+ Icon: string;
42
+ }>;
43
+ }
44
+ export interface SearchPackagesParams {
45
+ query: string;
46
+ type?: PackageType;
47
+ category?: string;
48
+ tag?: string;
49
+ order?: string;
50
+ take?: number;
51
+ skip?: number;
52
+ }
53
+ export declare function searchPackages(params: SearchPackagesParams): Promise<SearchPackagesResponse>;
@@ -0,0 +1,26 @@
1
+ const UGC_API_BASE = 'https://services.facepunch.com/sbox/package';
2
+ export const PACKAGE_TYPES = [
3
+ 'model', 'sound', 'material', 'map', 'game',
4
+ 'clothing', 'library', 'vsnd', 'prefab', 'shader',
5
+ ];
6
+ export async function searchPackages(params) {
7
+ const url = new URL(`${UGC_API_BASE}/find`);
8
+ url.searchParams.set('q', params.query);
9
+ if (params.type)
10
+ url.searchParams.set('type', params.type);
11
+ if (params.category)
12
+ url.searchParams.set('category', params.category);
13
+ if (params.tag)
14
+ url.searchParams.set('tag', params.tag);
15
+ if (params.order)
16
+ url.searchParams.set('order', params.order);
17
+ if (params.take)
18
+ url.searchParams.set('take', String(params.take));
19
+ if (params.skip)
20
+ url.searchParams.set('skip', String(params.skip));
21
+ const res = await fetch(url.toString());
22
+ if (!res.ok) {
23
+ throw new Error(`UGC API error: ${res.status} ${res.statusText}`);
24
+ }
25
+ return res.json();
26
+ }
@@ -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
@@ -9,6 +9,10 @@ import { registerSearchMembers } from './tools/search-members.js';
9
9
  import { registerListNamespaces } from './tools/list-namespaces.js';
10
10
  import { registerSearchDocs } from './tools/search-docs.js';
11
11
  import { registerUpdateSource } from './tools/update-source.js';
12
+ import { registerSearchPackages } from './tools/search-packages.js';
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';
12
16
  async function main() {
13
17
  console.error('[sbox-api] Starting S&box API MCP Server...');
14
18
  // Load API data
@@ -26,6 +30,12 @@ async function main() {
26
30
  registerListNamespaces(server, indexes);
27
31
  registerSearchDocs(server, indexes);
28
32
  registerUpdateSource(server, indexes);
33
+ // UGC Workshop tools (no indexes needed - live API)
34
+ registerSearchPackages(server);
35
+ registerGetPackage(server);
36
+ // Wiki documentation tools (live API, in-memory cached index)
37
+ registerSearchWiki(server);
38
+ registerGetWikiPage(server);
29
39
  // Connect via stdio
30
40
  const transport = new StdioServerTransport();
31
41
  await server.connect(transport);
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGetPackage(server: McpServer): void;
@@ -0,0 +1,87 @@
1
+ import { z } from 'zod';
2
+ import { searchPackages } from '../data/ugc-client.js';
3
+ function formatPackageDetails(pkg) {
4
+ const sections = [];
5
+ sections.push(`# ${pkg.Title}`);
6
+ sections.push(`**Identifier:** \`${pkg.FullIdent}\``);
7
+ sections.push(`**Type:** ${pkg.TypeName}`);
8
+ if (pkg.Summary)
9
+ sections.push(`**Summary:** ${pkg.Summary}`);
10
+ sections.push(`**Author:** ${pkg.Org.Title} (${pkg.Org.Ident})`);
11
+ sections.push('');
12
+ sections.push('## Stats');
13
+ if (pkg.VotesUp)
14
+ sections.push(`- Votes: ${pkg.VotesUp}`);
15
+ if (pkg.Favourited)
16
+ sections.push(`- Favourited: ${pkg.Favourited}`);
17
+ if (pkg.Collections)
18
+ sections.push(`- In collections: ${pkg.Collections}`);
19
+ if (pkg.Referenced)
20
+ sections.push(`- Referenced by: ${pkg.Referenced}`);
21
+ sections.push('');
22
+ sections.push('## Dates');
23
+ sections.push(`- Created: ${pkg.Created}`);
24
+ sections.push(`- Updated: ${pkg.Updated}`);
25
+ if (pkg.Tags?.length) {
26
+ sections.push('');
27
+ sections.push(`## Tags\n${pkg.Tags.join(', ')}`);
28
+ }
29
+ if (pkg.Thumb) {
30
+ sections.push('');
31
+ sections.push(`## Thumbnail\n${pkg.Thumb}`);
32
+ }
33
+ sections.push('');
34
+ sections.push(`## Usage\nUse \`${pkg.FullIdent}\` in your S&box project to reference this package.`);
35
+ sections.push(`Workshop page: https://sbox.game/${pkg.Org.Ident}/${pkg.Ident}`);
36
+ return sections.join('\n');
37
+ }
38
+ export function registerGetPackage(server) {
39
+ server.registerTool('get_package', {
40
+ title: 'Get S&box Package Details',
41
+ description: 'Get detailed information about a specific S&box Workshop package by its identifier (e.g., "facepunch.zombiemale"). Returns full metadata including stats, tags, author info, and usage instructions.',
42
+ inputSchema: {
43
+ ident: z
44
+ .string()
45
+ .describe("Package identifier in 'org.name' format (e.g., 'facepunch.zombiemale', 'fpopium.chair_01')"),
46
+ },
47
+ }, async ({ ident }) => {
48
+ try {
49
+ // The API doesn't have a direct get-by-id endpoint,
50
+ // so we search with the exact ident and filter
51
+ const parts = ident.split('.');
52
+ const searchQuery = parts.length > 1 ? parts[1] : ident;
53
+ const response = await searchPackages({ query: searchQuery, take: 20 });
54
+ const pkg = response.Packages.find(p => p.FullIdent.toLowerCase() === ident.toLowerCase());
55
+ if (!pkg) {
56
+ // Try broader search
57
+ const broader = await searchPackages({ query: ident, take: 20 });
58
+ const found = broader.Packages.find(p => p.FullIdent.toLowerCase() === ident.toLowerCase());
59
+ if (!found) {
60
+ const suggestions = response.Packages.slice(0, 5)
61
+ .map(p => ` - ${p.FullIdent} ("${p.Title}")`)
62
+ .join('\n');
63
+ return {
64
+ content: [{
65
+ type: 'text',
66
+ text: `Package "${ident}" not found.\n\nSimilar packages:\n${suggestions}`,
67
+ }],
68
+ };
69
+ }
70
+ return {
71
+ content: [{ type: 'text', text: formatPackageDetails(found) }],
72
+ };
73
+ }
74
+ return {
75
+ content: [{ type: 'text', text: formatPackageDetails(pkg) }],
76
+ };
77
+ }
78
+ catch (err) {
79
+ return {
80
+ content: [{
81
+ type: 'text',
82
+ text: `Error fetching package: ${err instanceof Error ? err.message : String(err)}`,
83
+ }],
84
+ };
85
+ }
86
+ });
87
+ }
@@ -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 registerSearchPackages(server: McpServer): void;
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ import { searchPackages, PACKAGE_TYPES } from '../data/ugc-client.js';
3
+ function formatPackage(pkg, index) {
4
+ const lines = [];
5
+ const votes = pkg.VotesUp ? ` | Votes: ${pkg.VotesUp}` : '';
6
+ const favs = pkg.Favourited ? ` | Favs: ${pkg.Favourited}` : '';
7
+ const tags = pkg.Tags?.length ? `Tags: ${pkg.Tags.join(', ')}` : '';
8
+ lines.push(`${index + 1}. **${pkg.FullIdent}** — "${pkg.Title}"`);
9
+ if (pkg.Summary)
10
+ lines.push(` ${pkg.Summary}`);
11
+ lines.push(` Type: ${pkg.TypeName}${votes}${favs}`);
12
+ if (tags)
13
+ lines.push(` ${tags}`);
14
+ lines.push(` Use in code: \`${pkg.FullIdent}\``);
15
+ return lines.join('\n');
16
+ }
17
+ export function registerSearchPackages(server) {
18
+ server.registerTool('search_packages', {
19
+ title: 'Search S&box Workshop Packages',
20
+ description: 'Search the S&box Workshop (UGC) for community-made assets: models, sounds, materials, maps, prefabs, shaders, clothing, and more. Returns packages with their identifiers that can be used directly in S&box game code.',
21
+ inputSchema: {
22
+ query: z.string().describe("Search query (e.g., 'zombie', 'wooden chair', 'footstep sound')"),
23
+ type: z
24
+ .enum(PACKAGE_TYPES)
25
+ .optional()
26
+ .describe('Filter by asset type: model, sound, material, map, game, clothing, library, vsnd, prefab, shader'),
27
+ category: z
28
+ .string()
29
+ .optional()
30
+ .describe('Filter by category (for models: Animal, Architecture, Debris, Development, Fence/Wall, Food, Furniture, Human, Lighting, Nature, Prop, Toy, Vehicle, Weapon)'),
31
+ tag: z.string().optional().describe("Filter by tag (e.g., 'kenney', 'retro', 'psx', 'realistic')"),
32
+ order: z
33
+ .enum(['popular', 'newest', 'updated', 'trending', 'thumbsup', 'favourites'])
34
+ .optional()
35
+ .describe('Sort order. Defaults to relevance.'),
36
+ take: z.number().min(1).max(50).default(10).describe('Max results (1-50, default 10)'),
37
+ },
38
+ }, async ({ query, type, category, tag, order, take }) => {
39
+ try {
40
+ const response = await searchPackages({ query, type, category, tag, order, take });
41
+ if (response.Packages.length === 0) {
42
+ return {
43
+ content: [{
44
+ type: 'text',
45
+ text: `No packages found matching "${query}"${type ? ` (type: ${type})` : ''}. Try broadening your search or removing filters.`,
46
+ }],
47
+ };
48
+ }
49
+ const formatted = response.Packages.map((pkg, i) => formatPackage(pkg, i));
50
+ const typeLabel = type ? ` (type: ${type})` : '';
51
+ const header = `Found ${response.TotalCount} packages matching "${query}"${typeLabel} (showing ${response.Packages.length}):\n`;
52
+ const topTags = response.Tags
53
+ ? Object.entries(response.Tags)
54
+ .sort(([, a], [, b]) => b - a)
55
+ .slice(0, 10)
56
+ .map(([t, c]) => `${t} (${c})`)
57
+ .join(', ')
58
+ : null;
59
+ const footer = topTags ? `\n---\nTop tags in results: ${topTags}` : '';
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: `${header}\n${formatted.join('\n\n')}${footer}`,
64
+ }],
65
+ };
66
+ }
67
+ catch (err) {
68
+ return {
69
+ content: [{
70
+ type: 'text',
71
+ text: `Error searching packages: ${err instanceof Error ? err.message : String(err)}`,
72
+ }],
73
+ };
74
+ }
75
+ });
76
+ }
@@ -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.0.0",
4
- "description": "MCP server for S&box game engine API documentation lookup. Search types, methods, properties, and documentation across the entire S&box API.",
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"
@@ -25,7 +25,12 @@
25
25
  "api",
26
26
  "documentation",
27
27
  "claude",
28
- "claude-code"
28
+ "claude-code",
29
+ "wiki",
30
+ "ugc",
31
+ "workshop",
32
+ "assets",
33
+ "models"
29
34
  ],
30
35
  "author": "sofianebel",
31
36
  "license": "MIT",