sbox-api-mcp 1.0.0 → 1.1.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.
@@ -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
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ 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';
12
14
  async function main() {
13
15
  console.error('[sbox-api] Starting S&box API MCP Server...');
14
16
  // Load API data
@@ -26,6 +28,9 @@ async function main() {
26
28
  registerListNamespaces(server, indexes);
27
29
  registerSearchDocs(server, indexes);
28
30
  registerUpdateSource(server, indexes);
31
+ // UGC Workshop tools (no indexes needed - live API)
32
+ registerSearchPackages(server);
33
+ registerGetPackage(server);
29
34
  // Connect via stdio
30
35
  const transport = new StdioServerTransport();
31
36
  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 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
+ }
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.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.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sbox-api-mcp": "dist/index.js"
@@ -25,7 +25,11 @@
25
25
  "api",
26
26
  "documentation",
27
27
  "claude",
28
- "claude-code"
28
+ "claude-code",
29
+ "ugc",
30
+ "workshop",
31
+ "assets",
32
+ "models"
29
33
  ],
30
34
  "author": "sofianebel",
31
35
  "license": "MIT",