@blitz-planner/mcp-server 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 +23 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +21 -0
- package/dist/http.d.ts +15 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +73 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/media-url.d.ts +18 -0
- package/dist/media-url.d.ts.map +1 -0
- package/dist/media-url.js +116 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +237 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Blitz Planner MCP Server
|
|
2
|
+
|
|
3
|
+
Remote Streamable HTTP MCP server for Blitz Planner.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
BLITZ_API_BASE_URL=https://api.blitz-planner.com/v1 \
|
|
7
|
+
PORT=8787 \
|
|
8
|
+
blitz-planner-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
MCP clients call `POST /mcp` and must send their Blitz API key per request:
|
|
12
|
+
|
|
13
|
+
```http
|
|
14
|
+
Authorization: Bearer bp_live_...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or:
|
|
18
|
+
|
|
19
|
+
```http
|
|
20
|
+
x-blitz-api-key: bp_live_...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The server is intentionally multi-user safe: it does not use a shared Blitz API key.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CA0BjG"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function extractBlitzApiKey(headers) {
|
|
2
|
+
const getHeader = (name) => {
|
|
3
|
+
if (headers instanceof Headers) {
|
|
4
|
+
return headers.get(name) || undefined;
|
|
5
|
+
}
|
|
6
|
+
const value = headers[name] ||
|
|
7
|
+
headers[name.toLowerCase()] ||
|
|
8
|
+
headers[name.toUpperCase()];
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return typeof value[0] === "string" ? value[0] : undefined;
|
|
11
|
+
}
|
|
12
|
+
return typeof value === "string" ? value : undefined;
|
|
13
|
+
};
|
|
14
|
+
const explicit = getHeader("x-blitz-api-key");
|
|
15
|
+
if (explicit?.trim()) {
|
|
16
|
+
return explicit.trim();
|
|
17
|
+
}
|
|
18
|
+
const authorization = getHeader("authorization");
|
|
19
|
+
const match = authorization?.match(/^Bearer\s+(.+)$/i);
|
|
20
|
+
return match?.[1]?.trim();
|
|
21
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BlitzSdkFactory } from "./tools.js";
|
|
2
|
+
export interface CreateMcpHttpAppOptions {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
allowedHosts?: string[];
|
|
6
|
+
path?: string;
|
|
7
|
+
sdkFactory?: BlitzSdkFactory;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export interface StartMcpHttpServerOptions extends CreateMcpHttpAppOptions {
|
|
11
|
+
port?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function createMcpHttpApp(options?: CreateMcpHttpAppOptions): any;
|
|
14
|
+
export declare function startMcpHttpServer(options?: StartMcpHttpServerOptions): any;
|
|
15
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAaD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,GAAG,CAwD3E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,OAazE"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { extractBlitzApiKey } from "./auth.js";
|
|
4
|
+
import { createBlitzMcpServer } from "./tools.js";
|
|
5
|
+
function writeMcpError(res, status, code, message) {
|
|
6
|
+
res.status(status).json({
|
|
7
|
+
jsonrpc: "2.0",
|
|
8
|
+
error: {
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
},
|
|
12
|
+
id: null,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function createMcpHttpApp(options = {}) {
|
|
16
|
+
const mcpPath = options.path || "/mcp";
|
|
17
|
+
const app = createMcpExpressApp({
|
|
18
|
+
host: options.host || "0.0.0.0",
|
|
19
|
+
allowedHosts: options.allowedHosts,
|
|
20
|
+
});
|
|
21
|
+
app.get("/healthz", (_req, res) => {
|
|
22
|
+
res.json({ ok: true, name: "blitz-planner-mcp" });
|
|
23
|
+
});
|
|
24
|
+
app.post(mcpPath, async (req, res) => {
|
|
25
|
+
const apiKey = extractBlitzApiKey(req.headers);
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
writeMcpError(res, 401, -32001, "Missing Blitz Planner API key.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const server = createBlitzMcpServer({
|
|
31
|
+
apiKey,
|
|
32
|
+
baseUrl: options.baseUrl,
|
|
33
|
+
sdkFactory: options.sdkFactory,
|
|
34
|
+
fetch: options.fetch,
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const transport = new StreamableHTTPServerTransport({
|
|
38
|
+
sessionIdGenerator: undefined,
|
|
39
|
+
});
|
|
40
|
+
await server.connect(transport);
|
|
41
|
+
await transport.handleRequest(req, res, req.body);
|
|
42
|
+
res.on("close", () => {
|
|
43
|
+
void transport.close();
|
|
44
|
+
void server.close();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!res.headersSent) {
|
|
49
|
+
writeMcpError(res, 500, -32603, error instanceof Error ? error.message : "Internal MCP server error.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
app.get(mcpPath, (_req, res) => {
|
|
54
|
+
writeMcpError(res, 405, -32000, "Method not allowed.");
|
|
55
|
+
});
|
|
56
|
+
app.delete(mcpPath, (_req, res) => {
|
|
57
|
+
writeMcpError(res, 405, -32000, "Method not allowed.");
|
|
58
|
+
});
|
|
59
|
+
return app;
|
|
60
|
+
}
|
|
61
|
+
export function startMcpHttpServer(options = {}) {
|
|
62
|
+
const port = options.port || Number(process.env.PORT || 8787);
|
|
63
|
+
const host = options.host || process.env.HOST || "0.0.0.0";
|
|
64
|
+
const baseUrl = options.baseUrl || process.env.BLITZ_API_BASE_URL;
|
|
65
|
+
const app = createMcpHttpApp({
|
|
66
|
+
...options,
|
|
67
|
+
host,
|
|
68
|
+
baseUrl,
|
|
69
|
+
});
|
|
70
|
+
return app.listen(port, host, () => {
|
|
71
|
+
console.log(`Blitz Planner MCP server listening on http://${host}:${port}${options.path || "/mcp"}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createMcpHttpApp, startMcpHttpServer, type CreateMcpHttpAppOptions, type StartMcpHttpServerOptions } from "./http.js";
|
|
3
|
+
import { createBlitzMcpServer, BLITZ_MCP_TOOL_NAMES } from "./tools.js";
|
|
4
|
+
import { extractBlitzApiKey } from "./auth.js";
|
|
5
|
+
import { assertSafeRemoteUrl, downloadRemoteMedia, isPrivateAddress } from "./media-url.js";
|
|
6
|
+
export { BLITZ_MCP_TOOL_NAMES, assertSafeRemoteUrl, createBlitzMcpServer, createMcpHttpApp, downloadRemoteMedia, extractBlitzApiKey, isPrivateAddress, startMcpHttpServer, type CreateMcpHttpAppOptions, type StartMcpHttpServerOptions, };
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,GAC/B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createMcpHttpApp, startMcpHttpServer, } from "./http.js";
|
|
5
|
+
import { createBlitzMcpServer, BLITZ_MCP_TOOL_NAMES } from "./tools.js";
|
|
6
|
+
import { extractBlitzApiKey } from "./auth.js";
|
|
7
|
+
import { assertSafeRemoteUrl, downloadRemoteMedia, isPrivateAddress, } from "./media-url.js";
|
|
8
|
+
export { BLITZ_MCP_TOOL_NAMES, assertSafeRemoteUrl, createBlitzMcpServer, createMcpHttpApp, downloadRemoteMedia, extractBlitzApiKey, isPrivateAddress, startMcpHttpServer, };
|
|
9
|
+
const isDirectRun = process.argv[1] &&
|
|
10
|
+
resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1]);
|
|
11
|
+
if (isDirectRun) {
|
|
12
|
+
startMcpHttpServer();
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_REMOTE_MEDIA_BYTES: number;
|
|
2
|
+
export declare const MAX_REMOTE_MEDIA_BYTES: number;
|
|
3
|
+
export interface DownloadedRemoteMedia {
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
filename: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DownloadRemoteMediaOptions {
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
filename?: string;
|
|
13
|
+
mimeType?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function isPrivateAddress(address: string): boolean;
|
|
16
|
+
export declare function assertSafeRemoteUrl(rawUrl: string): Promise<URL>;
|
|
17
|
+
export declare function downloadRemoteMedia(rawUrl: string, options?: DownloadRemoteMediaOptions): Promise<DownloadedRemoteMedia>;
|
|
18
|
+
//# sourceMappingURL=media-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-url.d.ts","sourceRoot":"","sources":["../src/media-url.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,8BAA8B,QAAoB,CAAC;AAChE,eAAO,MAAM,sBAAsB,QAAqB,CAAC;AAEzD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiCzD;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAyBtE;AAgDD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,qBAAqB,CAAC,CA+BhC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
export const DEFAULT_MAX_REMOTE_MEDIA_BYTES = 100 * 1024 * 1024;
|
|
4
|
+
export const MAX_REMOTE_MEDIA_BYTES = 1024 * 1024 * 1024;
|
|
5
|
+
export function isPrivateAddress(address) {
|
|
6
|
+
if (address.includes(":")) {
|
|
7
|
+
const lower = address.toLowerCase();
|
|
8
|
+
return (lower === "::1" ||
|
|
9
|
+
lower === "0:0:0:0:0:0:0:1" ||
|
|
10
|
+
lower.startsWith("fc") ||
|
|
11
|
+
lower.startsWith("fd") ||
|
|
12
|
+
lower.startsWith("fe80:") ||
|
|
13
|
+
lower.startsWith("::ffff:127.") ||
|
|
14
|
+
lower.startsWith("::ffff:10.") ||
|
|
15
|
+
lower.startsWith("::ffff:192.168."));
|
|
16
|
+
}
|
|
17
|
+
const parts = address.split(".").map(Number);
|
|
18
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const a = parts[0] ?? -1;
|
|
22
|
+
const b = parts[1] ?? -1;
|
|
23
|
+
return (a === 0 ||
|
|
24
|
+
a === 10 ||
|
|
25
|
+
a === 127 ||
|
|
26
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
27
|
+
(a === 169 && b === 254) ||
|
|
28
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
29
|
+
(a === 192 && b === 168) ||
|
|
30
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
31
|
+
a >= 224);
|
|
32
|
+
}
|
|
33
|
+
export async function assertSafeRemoteUrl(rawUrl) {
|
|
34
|
+
const url = new URL(rawUrl);
|
|
35
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
36
|
+
throw new Error("Remote media URL must use http or https.");
|
|
37
|
+
}
|
|
38
|
+
const hostname = url.hostname.toLowerCase();
|
|
39
|
+
if (hostname === "localhost" ||
|
|
40
|
+
hostname.endsWith(".localhost") ||
|
|
41
|
+
hostname.endsWith(".local")) {
|
|
42
|
+
throw new Error("Remote media URL cannot target localhost or local network hosts.");
|
|
43
|
+
}
|
|
44
|
+
const directIp = isIP(hostname);
|
|
45
|
+
const addresses = directIp
|
|
46
|
+
? [{ address: hostname }]
|
|
47
|
+
: await lookup(hostname, { all: true, verbatim: true });
|
|
48
|
+
if (!addresses.length || addresses.some((entry) => isPrivateAddress(entry.address))) {
|
|
49
|
+
throw new Error("Remote media URL resolves to a private or reserved network address.");
|
|
50
|
+
}
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
function filenameFromUrl(url) {
|
|
54
|
+
const lastSegment = decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() || "");
|
|
55
|
+
return lastSegment || "media";
|
|
56
|
+
}
|
|
57
|
+
async function readLimitedBytes(response, maxBytes) {
|
|
58
|
+
const reader = response.body?.getReader();
|
|
59
|
+
if (!reader) {
|
|
60
|
+
const buffer = await response.arrayBuffer();
|
|
61
|
+
if (buffer.byteLength > maxBytes) {
|
|
62
|
+
throw new Error(`Remote media exceeds the ${maxBytes} byte limit.`);
|
|
63
|
+
}
|
|
64
|
+
return new Uint8Array(buffer);
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
while (true) {
|
|
69
|
+
const { done, value } = await reader.read();
|
|
70
|
+
if (done) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
total += value.byteLength;
|
|
74
|
+
if (total > maxBytes) {
|
|
75
|
+
throw new Error(`Remote media exceeds the ${maxBytes} byte limit.`);
|
|
76
|
+
}
|
|
77
|
+
chunks.push(value);
|
|
78
|
+
}
|
|
79
|
+
const bytes = new Uint8Array(total);
|
|
80
|
+
let offset = 0;
|
|
81
|
+
for (const chunk of chunks) {
|
|
82
|
+
bytes.set(chunk, offset);
|
|
83
|
+
offset += chunk.byteLength;
|
|
84
|
+
}
|
|
85
|
+
return bytes;
|
|
86
|
+
}
|
|
87
|
+
function assertSupportedMimeType(mimeType) {
|
|
88
|
+
if (!mimeType.startsWith("image/") && !mimeType.startsWith("video/")) {
|
|
89
|
+
throw new Error(`Remote media MIME type ${mimeType} is not supported.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function downloadRemoteMedia(rawUrl, options = {}) {
|
|
93
|
+
const url = await assertSafeRemoteUrl(rawUrl);
|
|
94
|
+
const maxBytes = Math.min(Math.max(options.maxBytes || DEFAULT_MAX_REMOTE_MEDIA_BYTES, 1), MAX_REMOTE_MEDIA_BYTES);
|
|
95
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
96
|
+
const response = await fetchImpl(url, {
|
|
97
|
+
method: "GET",
|
|
98
|
+
redirect: "follow",
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`Failed to fetch remote media: HTTP ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
const contentLength = response.headers.get("content-length");
|
|
104
|
+
if (contentLength && Number(contentLength) > maxBytes) {
|
|
105
|
+
throw new Error(`Remote media exceeds the ${maxBytes} byte limit.`);
|
|
106
|
+
}
|
|
107
|
+
const mimeType = options.mimeType || response.headers.get("content-type")?.split(";")[0]?.trim() || "";
|
|
108
|
+
assertSupportedMimeType(mimeType);
|
|
109
|
+
const bytes = await readLimitedBytes(response, maxBytes);
|
|
110
|
+
return {
|
|
111
|
+
bytes,
|
|
112
|
+
filename: options.filename || filenameFromUrl(url),
|
|
113
|
+
mimeType,
|
|
114
|
+
size: bytes.byteLength,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BlitzPlanner, BlitzPlannerOptions } from "@blitz-planner/sdk";
|
|
3
|
+
export type BlitzSdkFactory = (options: BlitzPlannerOptions) => BlitzPlanner;
|
|
4
|
+
export interface CreateBlitzMcpServerOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
sdkFactory?: BlitzSdkFactory;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare const BLITZ_MCP_TOOL_NAMES: readonly ["blitz_list_platforms", "blitz_list_projects", "blitz_list_accounts", "blitz_create_connection_links", "blitz_get_capabilities", "blitz_upload_media_from_urls", "blitz_create_post", "blitz_schedule_post", "blitz_publish_post_now", "blitz_publish_existing_post", "blitz_get_calendar", "blitz_get_history"];
|
|
11
|
+
export declare function createBlitzMcpServer(options: CreateBlitzMcpServerOptions): McpServer;
|
|
12
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAqC,MAAM,oBAAoB,CAAC;AAI1G,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,YAAY,CAAC;AAE7E,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,eAAO,MAAM,oBAAoB,4TAavB,CAAC;AAwFX,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,SAAS,CA8MpF"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BlitzPlanner } from "@blitz-planner/sdk";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
import { downloadRemoteMedia } from "./media-url.js";
|
|
5
|
+
export const BLITZ_MCP_TOOL_NAMES = [
|
|
6
|
+
"blitz_list_platforms",
|
|
7
|
+
"blitz_list_projects",
|
|
8
|
+
"blitz_list_accounts",
|
|
9
|
+
"blitz_create_connection_links",
|
|
10
|
+
"blitz_get_capabilities",
|
|
11
|
+
"blitz_upload_media_from_urls",
|
|
12
|
+
"blitz_create_post",
|
|
13
|
+
"blitz_schedule_post",
|
|
14
|
+
"blitz_publish_post_now",
|
|
15
|
+
"blitz_publish_existing_post",
|
|
16
|
+
"blitz_get_calendar",
|
|
17
|
+
"blitz_get_history",
|
|
18
|
+
];
|
|
19
|
+
const socialPlatform = z.enum([
|
|
20
|
+
"LINKEDIN",
|
|
21
|
+
"FACEBOOK",
|
|
22
|
+
"INSTAGRAM",
|
|
23
|
+
"TIKTOK",
|
|
24
|
+
"TWITTER",
|
|
25
|
+
"YOUTUBE",
|
|
26
|
+
"THREADS",
|
|
27
|
+
"PINTEREST",
|
|
28
|
+
]);
|
|
29
|
+
const mediaType = z.enum([
|
|
30
|
+
"IMAGE",
|
|
31
|
+
"VIDEO",
|
|
32
|
+
"CAROUSEL",
|
|
33
|
+
"STORY",
|
|
34
|
+
"REEL",
|
|
35
|
+
"ARTICLE",
|
|
36
|
+
"DOCUMENT",
|
|
37
|
+
"LIVE",
|
|
38
|
+
]);
|
|
39
|
+
const postStatus = z.enum([
|
|
40
|
+
"DRAFT",
|
|
41
|
+
"PENDING_APPROVAL",
|
|
42
|
+
"APPROVED",
|
|
43
|
+
"SCHEDULED",
|
|
44
|
+
"PUBLISHING",
|
|
45
|
+
"PUBLISHED",
|
|
46
|
+
"FAILED",
|
|
47
|
+
"DELETED",
|
|
48
|
+
"PARTIALLY_PUBLISHED",
|
|
49
|
+
]);
|
|
50
|
+
const tiktokSettingsSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
privacyLevel: z.string().optional(),
|
|
53
|
+
disableComment: z.boolean().optional(),
|
|
54
|
+
disableDuet: z.boolean().optional(),
|
|
55
|
+
disableStitch: z.boolean().optional(),
|
|
56
|
+
autoAddMusic: z.boolean().optional(),
|
|
57
|
+
photoCoverIndex: z.number().int().min(0).optional(),
|
|
58
|
+
postMode: z.string().optional(),
|
|
59
|
+
})
|
|
60
|
+
.optional();
|
|
61
|
+
const createPostShape = {
|
|
62
|
+
projectId: z.string().optional(),
|
|
63
|
+
content: z.string().min(1),
|
|
64
|
+
title: z.string().optional(),
|
|
65
|
+
hashtags: z.array(z.string()).optional(),
|
|
66
|
+
mentions: z.array(z.string()).optional(),
|
|
67
|
+
tags: z.array(z.string()).optional(),
|
|
68
|
+
mediaUrls: z.array(z.url()).optional(),
|
|
69
|
+
mediaIds: z.array(z.string()).optional(),
|
|
70
|
+
mediaType: mediaType.optional(),
|
|
71
|
+
thumbnailUrl: z.url().optional(),
|
|
72
|
+
scheduledAt: z.string().datetime().optional(),
|
|
73
|
+
scheduledTimezone: z.string().optional(),
|
|
74
|
+
accountIds: z.array(z.string()).optional(),
|
|
75
|
+
platform: socialPlatform.optional(),
|
|
76
|
+
publishNow: z.boolean().optional(),
|
|
77
|
+
externalId: z.string().optional(),
|
|
78
|
+
sourceRef: z.string().optional(),
|
|
79
|
+
tiktok: tiktokSettingsSchema,
|
|
80
|
+
accountOverrides: z.record(z.string(), z.object({
|
|
81
|
+
content: z.string().optional(),
|
|
82
|
+
hashtags: z.array(z.string()).optional(),
|
|
83
|
+
platformSettings: z.record(z.string(), z.unknown()).optional(),
|
|
84
|
+
})).optional(),
|
|
85
|
+
};
|
|
86
|
+
function jsonContent(data) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify(data, null, 2),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function createBlitzMcpServer(options) {
|
|
97
|
+
const sdkFactory = options.sdkFactory || ((clientOptions) => new BlitzPlanner(clientOptions));
|
|
98
|
+
const sdk = sdkFactory({
|
|
99
|
+
apiKey: options.apiKey,
|
|
100
|
+
baseUrl: options.baseUrl,
|
|
101
|
+
fetch: options.fetch,
|
|
102
|
+
});
|
|
103
|
+
const server = new McpServer({
|
|
104
|
+
name: "blitz-planner",
|
|
105
|
+
version: "0.1.0",
|
|
106
|
+
});
|
|
107
|
+
server.registerTool("blitz_list_platforms", {
|
|
108
|
+
title: "List Blitz Planner platforms",
|
|
109
|
+
description: "List the social platforms configured in Blitz Planner.",
|
|
110
|
+
inputSchema: {},
|
|
111
|
+
}, async () => jsonContent(await sdk.platforms()));
|
|
112
|
+
server.registerTool("blitz_list_projects", {
|
|
113
|
+
title: "List Blitz Planner projects",
|
|
114
|
+
description: "List projects available to the Blitz API key.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
organizationId: z.string().optional(),
|
|
117
|
+
},
|
|
118
|
+
}, async ({ organizationId }) => jsonContent(await sdk.projects.list({ organizationId })));
|
|
119
|
+
server.registerTool("blitz_list_accounts", {
|
|
120
|
+
title: "List Blitz Planner social accounts",
|
|
121
|
+
description: "List connected social accounts available to the Blitz API key.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
organizationId: z.string().optional(),
|
|
124
|
+
projectId: z.string().optional(),
|
|
125
|
+
platform: socialPlatform.optional(),
|
|
126
|
+
status: z.enum(["ACTIVE", "EXPIRED", "ERROR", "REVOKED"]).optional(),
|
|
127
|
+
},
|
|
128
|
+
}, async (input) => jsonContent(await sdk.accounts.list(input)));
|
|
129
|
+
server.registerTool("blitz_create_connection_links", {
|
|
130
|
+
title: "Create Blitz Planner connection links",
|
|
131
|
+
description: "Create one or many OAuth URLs for users to connect social accounts.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
platforms: z.array(socialPlatform).min(1),
|
|
134
|
+
organizationId: z.string().optional(),
|
|
135
|
+
projectId: z.string().optional(),
|
|
136
|
+
returnUrl: z.url().optional(),
|
|
137
|
+
},
|
|
138
|
+
}, async (input) => jsonContent(await sdk.connections.createLinks(input)));
|
|
139
|
+
server.registerTool("blitz_get_capabilities", {
|
|
140
|
+
title: "Get Blitz Planner publishing capabilities",
|
|
141
|
+
description: "Read account-specific publishing capabilities for a project and optional media IDs.",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
projectId: z.string().optional(),
|
|
144
|
+
mediaIds: z.array(z.string()).optional(),
|
|
145
|
+
},
|
|
146
|
+
}, async (input) => jsonContent(await sdk.capabilities.get(input)));
|
|
147
|
+
server.registerTool("blitz_upload_media_from_urls", {
|
|
148
|
+
title: "Upload remote media URLs to Blitz Planner",
|
|
149
|
+
description: "Fetch public image/video URLs safely, upload them to Blitz Planner, and return media IDs.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
projectId: z.string().optional(),
|
|
152
|
+
maxBytes: z.number().int().min(1).optional(),
|
|
153
|
+
media: z.array(z.object({
|
|
154
|
+
url: z.url(),
|
|
155
|
+
filename: z.string().optional(),
|
|
156
|
+
mimeType: z.string().optional(),
|
|
157
|
+
altText: z.string().optional(),
|
|
158
|
+
width: z.number().int().min(1).optional(),
|
|
159
|
+
height: z.number().int().min(1).optional(),
|
|
160
|
+
duration: z.number().min(0).optional(),
|
|
161
|
+
})).min(1),
|
|
162
|
+
},
|
|
163
|
+
}, async ({ projectId, media, maxBytes }) => {
|
|
164
|
+
const uploads = [];
|
|
165
|
+
for (const item of media) {
|
|
166
|
+
const downloaded = await downloadRemoteMedia(item.url, {
|
|
167
|
+
fetch: options.fetch,
|
|
168
|
+
maxBytes,
|
|
169
|
+
filename: item.filename,
|
|
170
|
+
mimeType: item.mimeType,
|
|
171
|
+
});
|
|
172
|
+
uploads.push({
|
|
173
|
+
projectId,
|
|
174
|
+
filename: downloaded.filename,
|
|
175
|
+
mimeType: downloaded.mimeType,
|
|
176
|
+
size: downloaded.size,
|
|
177
|
+
body: downloaded.bytes,
|
|
178
|
+
altText: item.altText,
|
|
179
|
+
width: item.width,
|
|
180
|
+
height: item.height,
|
|
181
|
+
duration: item.duration,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return jsonContent(await sdk.media.uploadMany(uploads));
|
|
185
|
+
});
|
|
186
|
+
server.registerTool("blitz_create_post", {
|
|
187
|
+
title: "Create a Blitz Planner post",
|
|
188
|
+
description: "Create a draft or API-created post. Use scheduledAt for planning or publishNow for instant posting.",
|
|
189
|
+
inputSchema: createPostShape,
|
|
190
|
+
}, async (input) => jsonContent(await sdk.posts.create(input)));
|
|
191
|
+
server.registerTool("blitz_schedule_post", {
|
|
192
|
+
title: "Schedule a Blitz Planner post",
|
|
193
|
+
description: "Create a Blitz Planner post scheduled for a future ISO timestamp.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
...createPostShape,
|
|
196
|
+
scheduledAt: z.string().datetime(),
|
|
197
|
+
},
|
|
198
|
+
}, async (input) => jsonContent(await sdk.schedulePost(input)));
|
|
199
|
+
server.registerTool("blitz_publish_post_now", {
|
|
200
|
+
title: "Publish a new Blitz Planner post now",
|
|
201
|
+
description: "Create a Blitz Planner post and immediately request publishing.",
|
|
202
|
+
inputSchema: createPostShape,
|
|
203
|
+
}, async (input) => jsonContent(await sdk.publishNow(input)));
|
|
204
|
+
server.registerTool("blitz_publish_existing_post", {
|
|
205
|
+
title: "Publish an existing Blitz Planner post",
|
|
206
|
+
description: "Request publishing for an existing Blitz Planner post ID.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
postId: z.string(),
|
|
209
|
+
},
|
|
210
|
+
}, async ({ postId }) => jsonContent(await sdk.posts.publish(postId)));
|
|
211
|
+
server.registerTool("blitz_get_calendar", {
|
|
212
|
+
title: "Get Blitz Planner calendar",
|
|
213
|
+
description: "Read scheduled posts from a project or organization calendar.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
organizationId: z.string().optional(),
|
|
216
|
+
projectId: z.string().optional(),
|
|
217
|
+
platform: socialPlatform.optional(),
|
|
218
|
+
startDate: z.string().datetime().optional(),
|
|
219
|
+
endDate: z.string().datetime().optional(),
|
|
220
|
+
},
|
|
221
|
+
}, async (input) => jsonContent(await sdk.calendar.list(input)));
|
|
222
|
+
server.registerTool("blitz_get_history", {
|
|
223
|
+
title: "Get Blitz Planner publishing history",
|
|
224
|
+
description: "Read publishing history from Blitz Planner.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
organizationId: z.string().optional(),
|
|
227
|
+
projectId: z.string().optional(),
|
|
228
|
+
platform: socialPlatform.optional(),
|
|
229
|
+
accountId: z.string().optional(),
|
|
230
|
+
startDate: z.string().datetime().optional(),
|
|
231
|
+
endDate: z.string().datetime().optional(),
|
|
232
|
+
limit: z.number().int().min(1).optional(),
|
|
233
|
+
offset: z.number().int().min(0).optional(),
|
|
234
|
+
},
|
|
235
|
+
}, async (input) => jsonContent(await sdk.history.list(input)));
|
|
236
|
+
return server;
|
|
237
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blitz-planner/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remote Streamable HTTP MCP server for Blitz Planner.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"blitz-planner-mcp": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"zod": "^3.25.76",
|
|
29
|
+
"@blitz-planner/sdk": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^24.7.1",
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"test": "tsx --test src/*.test.ts"
|
|
43
|
+
}
|
|
44
|
+
}
|