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.
- package/dist/data/ugc-client.d.ts +53 -0
- package/dist/data/ugc-client.js +26 -0
- package/dist/index.js +5 -0
- package/dist/tools/get-package.d.ts +2 -0
- package/dist/tools/get-package.js +87 -0
- package/dist/tools/search-packages.d.ts +2 -0
- package/dist/tools/search-packages.js +76 -0
- package/package.json +7 -3
|
@@ -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,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,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.
|
|
4
|
-
"description": "MCP server for S&box game engine API documentation
|
|
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",
|