@gritsenko/cta-mcp 0.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/README.md +43 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +157 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @gritsenko/cta-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) (Model Context Protocol) stdio server
|
|
4
|
+
that exposes the headless playable-ad packer to agents and IDEs. It wraps
|
|
5
|
+
[`@gritsenko/cta-pack`](https://www.npmjs.com/package/@gritsenko/cta-pack) — the
|
|
6
|
+
same engine behind the [PlayableTools](https://tools.gritsenko.biz/) `/publish`
|
|
7
|
+
page — so an agent can pack an already-built HTML5 playable into per-network
|
|
8
|
+
builds and get a machine-readable validation report. No browser, no clicks.
|
|
9
|
+
|
|
10
|
+
## Install & register
|
|
11
|
+
|
|
12
|
+
Register the stdio server in your MCP client (Claude Desktop, Cursor, etc.):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"cta": { "command": "npx", "args": ["-y", "@gritsenko/cta-mcp"] }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install it and point at the binary:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install -g @gritsenko/cta-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "mcpServers": { "cta": { "command": "cta-mcp" } } }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Tools
|
|
33
|
+
|
|
34
|
+
- `list_networks()` → `[{ id, output, maxBytes, notes }]`
|
|
35
|
+
- `pack_playable({ source, networks?, outDir?, options?, validate? })` → `{ builds, report }`
|
|
36
|
+
- `validate_build({ source, networks? })` → `report`
|
|
37
|
+
|
|
38
|
+
`source` is a filesystem path **or** base64-encoded HTML, so an agent can pack a
|
|
39
|
+
build inline or from disk. `pack_playable` validates by default.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cta-mcp — MCP server exposing the headless playable packer over stdio.
|
|
3
|
+
// Thin wrapper over @gritsenko/cta-pack (the Node adapter). Three tools:
|
|
4
|
+
// list_networks() · pack_playable({...}) · validate_build({...})
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { getNetworks, pack, runPack, validateBuild, bundleFromBase64Html, } from "@gritsenko/cta-pack";
|
|
10
|
+
const optionsSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
name: z.string().optional(),
|
|
13
|
+
suffix: z.string().optional(),
|
|
14
|
+
storeUrls: z
|
|
15
|
+
.object({ android: z.string().optional(), ios: z.string().optional() })
|
|
16
|
+
.optional(),
|
|
17
|
+
compress: z.enum(["none", "imba"]).optional(),
|
|
18
|
+
imbaEncoding: z.enum(["base64", "base122"]).optional(),
|
|
19
|
+
})
|
|
20
|
+
.optional();
|
|
21
|
+
function json(value) {
|
|
22
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
23
|
+
}
|
|
24
|
+
function fail(error) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
28
|
+
],
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function isPath(source) {
|
|
33
|
+
try {
|
|
34
|
+
await stat(source);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function looksBase64(value) {
|
|
42
|
+
const t = value.trim();
|
|
43
|
+
return t.length >= 8 && t.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(t);
|
|
44
|
+
}
|
|
45
|
+
function looksHtml(value) {
|
|
46
|
+
return /<\s*(?:!doctype|html|head|body|div|script|canvas|meta|section|main|a\b|img)/i.test(value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve `source` (an existing filesystem path OR base64-encoded HTML) with strict
|
|
50
|
+
* validation, so arbitrary non-base64 strings are rejected instead of silently
|
|
51
|
+
* decoded into garbage.
|
|
52
|
+
*/
|
|
53
|
+
async function resolveSource(source) {
|
|
54
|
+
if (await isPath(source)) {
|
|
55
|
+
return { kind: "path", path: source };
|
|
56
|
+
}
|
|
57
|
+
if (looksHtml(source)) {
|
|
58
|
+
throw new Error("`source` looks like raw HTML; pass it base64-encoded, or a filesystem path.");
|
|
59
|
+
}
|
|
60
|
+
if (!looksBase64(source)) {
|
|
61
|
+
throw new Error("`source` is neither an existing path nor base64-encoded HTML.");
|
|
62
|
+
}
|
|
63
|
+
const bundle = bundleFromBase64Html(source);
|
|
64
|
+
if (!looksHtml(bundle.entryHtml)) {
|
|
65
|
+
throw new Error("Decoded base64 `source` does not look like HTML.");
|
|
66
|
+
}
|
|
67
|
+
return { kind: "bundle", bundle };
|
|
68
|
+
}
|
|
69
|
+
function buildsFromReport(report) {
|
|
70
|
+
return report.networks.map((n) => ({
|
|
71
|
+
network: n.id,
|
|
72
|
+
path: n.path,
|
|
73
|
+
sizeBytes: n.sizeBytes,
|
|
74
|
+
format: n.output,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const server = new McpServer({ name: "cta-mcp", version: "0.1.0" });
|
|
78
|
+
server.registerTool("list_networks", {
|
|
79
|
+
title: "List networks",
|
|
80
|
+
description: "List the supported ad networks with output type, max size and notes.",
|
|
81
|
+
inputSchema: {},
|
|
82
|
+
}, async () => {
|
|
83
|
+
const networks = getNetworks().map((n) => ({
|
|
84
|
+
id: n.id,
|
|
85
|
+
output: n.output,
|
|
86
|
+
maxBytes: n.constraints.maxBytes,
|
|
87
|
+
notes: n.notes,
|
|
88
|
+
}));
|
|
89
|
+
return json(networks);
|
|
90
|
+
});
|
|
91
|
+
server.registerTool("pack_playable", {
|
|
92
|
+
title: "Pack playable",
|
|
93
|
+
description: "Pack a playable for one or more ad networks and write the builds. `source` is a path " +
|
|
94
|
+
"(folder with index.html, or a single .html) OR base64-encoded HTML. Returns builds + report.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
source: z.string().describe("Filesystem path OR base64-encoded HTML."),
|
|
97
|
+
networks: z.array(z.string()).optional().describe("Network ids/names; omit for all."),
|
|
98
|
+
outDir: z.string().optional().describe("Output directory (default: ./builds)."),
|
|
99
|
+
validate: z.boolean().optional().describe("Run validation (default: true)."),
|
|
100
|
+
options: optionsSchema,
|
|
101
|
+
},
|
|
102
|
+
}, async ({ source, networks, outDir, validate, options }) => {
|
|
103
|
+
try {
|
|
104
|
+
const out = outDir ?? "builds";
|
|
105
|
+
const doValidate = validate ?? true;
|
|
106
|
+
const opts = (options ?? {});
|
|
107
|
+
const resolved = await resolveSource(source);
|
|
108
|
+
const report = resolved.kind === "path"
|
|
109
|
+
? await pack({ source: resolved.path, networks, out, validate: doValidate, options: opts })
|
|
110
|
+
: await runPack(resolved.bundle, {
|
|
111
|
+
networks,
|
|
112
|
+
out,
|
|
113
|
+
validate: doValidate,
|
|
114
|
+
options: { name: "Playable", ...opts },
|
|
115
|
+
});
|
|
116
|
+
return json({ builds: buildsFromReport(report), report });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return fail(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
server.registerTool("validate_build", {
|
|
123
|
+
title: "Validate build",
|
|
124
|
+
description: "Validate a playable for one or more networks WITHOUT writing builds. `source` is a path " +
|
|
125
|
+
"or base64-encoded HTML. Returns the report.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
source: z.string().describe("Filesystem path OR base64-encoded HTML."),
|
|
128
|
+
networks: z.array(z.string()).optional().describe("Network ids/names; omit for all."),
|
|
129
|
+
options: optionsSchema,
|
|
130
|
+
},
|
|
131
|
+
}, async ({ source, networks, options }) => {
|
|
132
|
+
try {
|
|
133
|
+
const opts = (options ?? {});
|
|
134
|
+
const resolved = await resolveSource(source);
|
|
135
|
+
const report = resolved.kind === "path"
|
|
136
|
+
? await validateBuild({ source: resolved.path, networks, options: opts })
|
|
137
|
+
: await runPack(resolved.bundle, {
|
|
138
|
+
networks,
|
|
139
|
+
validate: true,
|
|
140
|
+
options: { name: "Playable", ...opts },
|
|
141
|
+
});
|
|
142
|
+
return json(report);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return fail(error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
async function main() {
|
|
149
|
+
const transport = new StdioServerTransport();
|
|
150
|
+
await server.connect(transport);
|
|
151
|
+
// stderr is safe for logs; stdout is the MCP channel.
|
|
152
|
+
console.error("cta-mcp ready (stdio): list_networks, pack_playable, validate_build");
|
|
153
|
+
}
|
|
154
|
+
main().catch((error) => {
|
|
155
|
+
console.error(`cta-mcp fatal: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gritsenko/cta-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing the headless playable packer (list_networks / pack_playable / validate_build).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/server.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"cta-mcp": "./dist/server.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@gritsenko/cta-pack": "^0.1.0",
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
24
|
+
"zod": "^3.23.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.0.13",
|
|
28
|
+
"typescript": "~5.8.3"
|
|
29
|
+
}
|
|
30
|
+
}
|