@jclvsh/dropspace 2.23.0 → 2.24.1
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 +17 -0
- package/dist/client.d.ts +21 -1
- package/dist/client.js +45 -6
- package/dist/hosted-transport.d.ts +10 -0
- package/dist/hosted-transport.js +17 -0
- package/dist/hosted.d.ts +10 -0
- package/dist/hosted.js +9 -0
- package/dist/idempotency.d.ts +10 -0
- package/dist/idempotency.js +28 -0
- package/dist/index.js +2 -34
- package/dist/policy.d.ts +36 -0
- package/dist/policy.js +33 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +36 -0
- package/dist/tool-policy.d.ts +28 -0
- package/dist/tool-policy.js +143 -0
- package/dist/tools/launches.js +14 -2
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ This installs two binaries:
|
|
|
13
13
|
- `dropspace` — CLI for terminal use
|
|
14
14
|
- `dropspace-mcp` — MCP server for AI agents
|
|
15
15
|
|
|
16
|
+
Imported as a library, the package resolves only through its `exports` map (`.`, `./hosted`, `./tool-policy`, `./policy`, `./package.json`); deep imports of `dist/*` are not supported.
|
|
17
|
+
|
|
16
18
|
## CLI
|
|
17
19
|
|
|
18
20
|
```bash
|
|
@@ -107,6 +109,21 @@ Add to your `.cursor/mcp.json`:
|
|
|
107
109
|
}
|
|
108
110
|
```
|
|
109
111
|
|
|
112
|
+
## Hosted Endpoint
|
|
113
|
+
|
|
114
|
+
Clients that connect to a remote MCP server over HTTPS can skip the install and use:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
https://api.dropspace.dev/mcp
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- Transport: MCP streamable HTTP, `POST` only, stateless, JSON responses.
|
|
121
|
+
- Auth: `Authorization: Bearer ds_live_...` (the same API key; its scopes apply).
|
|
122
|
+
- Tools: a subset of this package. API key, webhook, boost, credit, and reddit engagement tools are only available through the npm package.
|
|
123
|
+
- Every tool is annotated by class: `read` (`readOnlyHint: true`), `write`, or `sensitive` (`destructiveHint: true`: it publishes, schedules, or deletes on a connected platform, or deletes a launch or persona with no restore). The class of each hosted tool is listed at https://www.dropspace.dev/docs#mcp-hosted, and the source of truth is `src/tool-policy.ts`.
|
|
124
|
+
- `create_launch` sends a content-derived `Idempotency-Key`, so a retry after a timeout returns the launch already created instead of creating a second one (the result says so).
|
|
125
|
+
- Each MCP request counts once against the key's rate limit, and each tool call counts once more when it reaches the REST API.
|
|
126
|
+
|
|
110
127
|
## Available Tools
|
|
111
128
|
|
|
112
129
|
### Launches
|
package/dist/client.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
+
export interface DropspaceClientOptions {
|
|
2
|
+
/** API key to send. Defaults to the DROPSPACE_API_KEY environment variable. */
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
/** REST base URL. Defaults to DROPSPACE_API_URL, then https://api.dropspace.dev. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
1
7
|
export declare class DropspaceClient {
|
|
2
8
|
private apiKey;
|
|
3
9
|
private baseUrl;
|
|
4
|
-
|
|
10
|
+
/** True when the base URL was injected; the network-error hint then names no URL or env var. */
|
|
11
|
+
private hasInjectedBaseUrl;
|
|
12
|
+
constructor(options?: DropspaceClientOptions);
|
|
5
13
|
get<T>(path: string, params?: Record<string, string>): Promise<T>;
|
|
6
14
|
post<T>(path: string, body?: unknown): Promise<T>;
|
|
7
15
|
put<T>(path: string, body: unknown): Promise<T>;
|
|
8
16
|
patch<T>(path: string, body: unknown): Promise<T>;
|
|
9
17
|
delete(path: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* POST with an Idempotency-Key. The API answers a repeat of the same key within 24 hours
|
|
20
|
+
* with the original response and `Idempotent-Replayed: true`; `replayed` surfaces that so
|
|
21
|
+
* the caller can say "nothing new was created".
|
|
22
|
+
*/
|
|
23
|
+
postIdempotent<T>(path: string, body: unknown, idempotencyKey: string): Promise<{
|
|
24
|
+
result: T;
|
|
25
|
+
replayed: boolean;
|
|
26
|
+
}>;
|
|
10
27
|
private request;
|
|
28
|
+
/** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
|
|
29
|
+
private readJson;
|
|
30
|
+
private send;
|
|
11
31
|
private handleError;
|
|
12
32
|
}
|
package/dist/client.js
CHANGED
|
@@ -2,16 +2,21 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
2
2
|
export class DropspaceClient {
|
|
3
3
|
apiKey;
|
|
4
4
|
baseUrl;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** True when the base URL was injected; the network-error hint then names no URL or env var. */
|
|
6
|
+
hasInjectedBaseUrl;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
const apiKey = options.apiKey ?? process.env.DROPSPACE_API_KEY;
|
|
7
9
|
if (!apiKey) {
|
|
8
10
|
throw new Error("DROPSPACE_API_KEY environment variable is required");
|
|
9
11
|
}
|
|
10
12
|
if (!apiKey.startsWith("ds_live_")) {
|
|
11
|
-
|
|
13
|
+
// An injected key came from a hosted caller, who never set the env var.
|
|
14
|
+
const subject = options.apiKey !== undefined ? "api key" : "DROPSPACE_API_KEY";
|
|
15
|
+
throw new Error(`${subject} must start with 'ds_live_'. Get one at https://www.dropspace.dev/settings`);
|
|
12
16
|
}
|
|
13
17
|
this.apiKey = apiKey;
|
|
14
|
-
this.
|
|
18
|
+
this.hasInjectedBaseUrl = Boolean(options.baseUrl);
|
|
19
|
+
this.baseUrl = (options.baseUrl || process.env.DROPSPACE_API_URL || "https://api.dropspace.dev").replace(/\/$/, "");
|
|
15
20
|
}
|
|
16
21
|
async get(path, params) {
|
|
17
22
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
@@ -63,7 +68,39 @@ export class DropspaceClient {
|
|
|
63
68
|
throw error;
|
|
64
69
|
}
|
|
65
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* POST with an Idempotency-Key. The API answers a repeat of the same key within 24 hours
|
|
73
|
+
* with the original response and `Idempotent-Replayed: true`; `replayed` surfaces that so
|
|
74
|
+
* the caller can say "nothing new was created".
|
|
75
|
+
*/
|
|
76
|
+
async postIdempotent(path, body, idempotencyKey) {
|
|
77
|
+
const response = await this.send(`${this.baseUrl}${path}`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
|
|
80
|
+
body: JSON.stringify(body),
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
result: await this.readJson(response),
|
|
84
|
+
replayed: response.headers.get("Idempotent-Replayed") === "true",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
66
87
|
async request(url, init) {
|
|
88
|
+
const response = await this.send(url, init);
|
|
89
|
+
return this.readJson(response);
|
|
90
|
+
}
|
|
91
|
+
/** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
|
|
92
|
+
async readJson(response) {
|
|
93
|
+
try {
|
|
94
|
+
return (await response.json());
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
98
|
+
throw new Error("request timed out after 30s");
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async send(url, init) {
|
|
67
104
|
let response;
|
|
68
105
|
try {
|
|
69
106
|
response = await fetch(url, {
|
|
@@ -80,14 +117,16 @@ export class DropspaceClient {
|
|
|
80
117
|
throw new Error("request timed out after 30s");
|
|
81
118
|
}
|
|
82
119
|
if (error instanceof TypeError) {
|
|
83
|
-
throw new Error(
|
|
120
|
+
throw new Error(this.hasInjectedBaseUrl
|
|
121
|
+
? `network error reaching the api: ${error.message}`
|
|
122
|
+
: `network error: ${error.message}. check DROPSPACE_API_URL and your connection`);
|
|
84
123
|
}
|
|
85
124
|
throw error;
|
|
86
125
|
}
|
|
87
126
|
if (!response.ok) {
|
|
88
127
|
await this.handleError(response);
|
|
89
128
|
}
|
|
90
|
-
return
|
|
129
|
+
return response;
|
|
91
130
|
}
|
|
92
131
|
async handleError(response) {
|
|
93
132
|
let errorMessage;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serve one MCP request over HTTP. LOCKSTEP across every apps/<app>/packages/mcp.
|
|
4
|
+
*
|
|
5
|
+
* Stateless: a fresh transport per request, no sessions, JSON responses - so it runs on
|
|
6
|
+
* serverless functions where consecutive requests from one client can land on different
|
|
7
|
+
* instances. Only POST is meaningful; the host must not route GET here (a stateless GET
|
|
8
|
+
* opens an event stream that never ends).
|
|
9
|
+
*/
|
|
10
|
+
export declare function serveStatelessMcp(server: McpServer, request: Request): Promise<Response>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serve one MCP request over HTTP. LOCKSTEP across every apps/<app>/packages/mcp.
|
|
4
|
+
*
|
|
5
|
+
* Stateless: a fresh transport per request, no sessions, JSON responses - so it runs on
|
|
6
|
+
* serverless functions where consecutive requests from one client can land on different
|
|
7
|
+
* instances. Only POST is meaningful; the host must not route GET here (a stateless GET
|
|
8
|
+
* opens an event stream that never ends).
|
|
9
|
+
*/
|
|
10
|
+
export async function serveStatelessMcp(server, request) {
|
|
11
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
12
|
+
sessionIdGenerator: undefined,
|
|
13
|
+
enableJsonResponse: true,
|
|
14
|
+
});
|
|
15
|
+
await server.connect(transport);
|
|
16
|
+
return transport.handleRequest(request);
|
|
17
|
+
}
|
package/dist/hosted.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface HostedMcpOptions {
|
|
2
|
+
/** The caller's dropspace API key. The host authenticates it before calling in. */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** REST base URL the tools call, e.g. https://api.dropspace.dev */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** Reported as serverInfo.version */
|
|
7
|
+
version: string;
|
|
8
|
+
}
|
|
9
|
+
/** One hosted MCP request: a per-request client and server over the stateless transport. */
|
|
10
|
+
export declare function handleHostedMcpRequest(request: Request, options: HostedMcpOptions): Promise<Response>;
|
package/dist/hosted.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DropspaceClient } from "./client.js";
|
|
2
|
+
import { serveStatelessMcp } from "./hosted-transport.js";
|
|
3
|
+
import { createDropspaceServer } from "./server.js";
|
|
4
|
+
/** One hosted MCP request: a per-request client and server over the stateless transport. */
|
|
5
|
+
export function handleHostedMcpRequest(request, options) {
|
|
6
|
+
const client = new DropspaceClient({ apiKey: options.apiKey, baseUrl: options.baseUrl });
|
|
7
|
+
const server = createDropspaceServer({ client, version: options.version, profile: "hosted" });
|
|
8
|
+
return serveStatelessMcp(server, request);
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A content-derived Idempotency-Key for create calls.
|
|
3
|
+
*
|
|
4
|
+
* The REST API dedupes a request that repeats a key within 24 hours (per key + endpoint)
|
|
5
|
+
* and answers with the original response plus `Idempotent-Replayed: true`. Deriving the
|
|
6
|
+
* key from the arguments means a retry after a timeout - by an agent, or by a person
|
|
7
|
+
* pressing "try again" in an MCP client - returns the record that was already created
|
|
8
|
+
* instead of creating a second one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function contentIdempotencyKey(args: unknown): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A content-derived Idempotency-Key for create calls.
|
|
3
|
+
*
|
|
4
|
+
* The REST API dedupes a request that repeats a key within 24 hours (per key + endpoint)
|
|
5
|
+
* and answers with the original response plus `Idempotent-Replayed: true`. Deriving the
|
|
6
|
+
* key from the arguments means a retry after a timeout - by an agent, or by a person
|
|
7
|
+
* pressing "try again" in an MCP client - returns the record that was already created
|
|
8
|
+
* instead of creating a second one.
|
|
9
|
+
*/
|
|
10
|
+
function stable(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(stable);
|
|
13
|
+
if (value !== null && typeof value === "object") {
|
|
14
|
+
const source = value;
|
|
15
|
+
const sorted = {};
|
|
16
|
+
for (const key of Object.keys(source).sort()) {
|
|
17
|
+
if (source[key] !== undefined)
|
|
18
|
+
sorted[key] = stable(source[key]);
|
|
19
|
+
}
|
|
20
|
+
return sorted;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export async function contentIdempotencyKey(args) {
|
|
25
|
+
const bytes = new TextEncoder().encode(JSON.stringify(stable(args)));
|
|
26
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
27
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
28
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
4
|
import { DropspaceClient } from "./client.js";
|
|
6
|
-
import {
|
|
7
|
-
import { registerPersonaTools } from "./tools/personas.js";
|
|
8
|
-
import { registerConnectionTools } from "./tools/connections.js";
|
|
9
|
-
import { registerKeyTools } from "./tools/keys.js";
|
|
10
|
-
import { registerWebhookTools } from "./tools/webhooks.js";
|
|
11
|
-
import { registerDropspaceTools } from "./tools/dropspace.js";
|
|
12
|
-
import { registerUsageTools } from "./tools/usage.js";
|
|
13
|
-
import { registerCreditTools } from "./tools/credits.js";
|
|
14
|
-
import { registerMediaTools } from "./tools/media.js";
|
|
15
|
-
import { registerBoostTools } from "./tools/boosts.js";
|
|
16
|
-
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
17
|
-
import { registerRecommendationTools } from "./tools/recommendations.js";
|
|
18
|
-
import { registerRedditTools } from "./tools/reddit.js";
|
|
19
|
-
import { registerAudienceTools } from "./tools/audience.js";
|
|
5
|
+
import { createDropspaceServer } from "./server.js";
|
|
20
6
|
const requireFromHere = createRequire(import.meta.url);
|
|
21
7
|
const pkg = requireFromHere("../package.json");
|
|
22
|
-
const server = new
|
|
23
|
-
name: "dropspace",
|
|
24
|
-
version: pkg.version,
|
|
25
|
-
});
|
|
26
|
-
const client = new DropspaceClient();
|
|
27
|
-
registerLaunchTools(server, client);
|
|
28
|
-
registerPersonaTools(server, client);
|
|
29
|
-
registerConnectionTools(server, client);
|
|
30
|
-
registerKeyTools(server, client);
|
|
31
|
-
registerWebhookTools(server, client);
|
|
32
|
-
registerDropspaceTools(server, client);
|
|
33
|
-
registerUsageTools(server, client);
|
|
34
|
-
registerCreditTools(server, client);
|
|
35
|
-
registerMediaTools(server, client);
|
|
36
|
-
registerBoostTools(server, client);
|
|
37
|
-
registerAutopilotTools(server, client);
|
|
38
|
-
registerRecommendationTools(server, client);
|
|
39
|
-
registerRedditTools(server, client);
|
|
40
|
-
registerAudienceTools(server, client);
|
|
8
|
+
const server = createDropspaceServer({ client: new DropspaceClient(), version: pkg.version, profile: "full" });
|
|
41
9
|
const transport = new StdioServerTransport();
|
|
42
10
|
await server.connect(transport);
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generic tool-policy machinery. LOCKSTEP across every apps/<app>/packages/mcp
|
|
4
|
+
* (byte-identical; scripts/check-lockstep-identity.sh enforces it). The per-app
|
|
5
|
+
* table lives in tool-policy.ts next to this file.
|
|
6
|
+
*
|
|
7
|
+
* class:
|
|
8
|
+
* read - retrieves information; changes nothing.
|
|
9
|
+
* write - changes the app's own data, reversibly; nothing leaves the app.
|
|
10
|
+
* sensitive - publishes, sends, or deletes on an external platform, deletes a record
|
|
11
|
+
* with no user-facing restore, spends money, handles credentials, or
|
|
12
|
+
* switches on automatic sending or posting.
|
|
13
|
+
* hosted: whether the app's hosted endpoint (api.<brand>/mcp) serves the tool.
|
|
14
|
+
* category: the documentation grouping every docs count is computed from.
|
|
15
|
+
*/
|
|
16
|
+
export type ToolClass = "read" | "write" | "sensitive";
|
|
17
|
+
export type ToolProfile = "full" | "hosted";
|
|
18
|
+
export interface ToolPolicy<Category extends string = string> {
|
|
19
|
+
class: ToolClass;
|
|
20
|
+
hosted: boolean;
|
|
21
|
+
category: Category;
|
|
22
|
+
}
|
|
23
|
+
export type PolicyTable<Category extends string = string> = Record<string, ToolPolicy<Category>>;
|
|
24
|
+
export declare function toolPolicyFor<C extends string>(table: PolicyTable<C>, name: string): ToolPolicy<C>;
|
|
25
|
+
export declare function isToolEnabledIn<C extends string>(table: PolicyTable<C>, name: string, profile: ToolProfile): boolean;
|
|
26
|
+
export declare function toolAnnotationsFor<C extends string>(table: PolicyTable<C>, name: string): {
|
|
27
|
+
readOnlyHint: boolean;
|
|
28
|
+
destructiveHint: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** Tool names in a category, in table order. */
|
|
31
|
+
export declare function toolsInCategoryOf<C extends string>(table: PolicyTable<C>, category: C): string[];
|
|
32
|
+
/**
|
|
33
|
+
* A stand-in for McpServer that every register*Tools(server, client) function can be
|
|
34
|
+
* handed. It is the one place a tool is filtered by profile and given its annotations.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPolicyRegistrar<C extends string>(server: McpServer, table: PolicyTable<C>, profile: ToolProfile): McpServer;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function toolPolicyFor(table, name) {
|
|
2
|
+
const policy = table[name];
|
|
3
|
+
if (!policy) {
|
|
4
|
+
throw new Error(`tool "${name}" has no entry in tool-policy.ts - classify it before registering`);
|
|
5
|
+
}
|
|
6
|
+
return policy;
|
|
7
|
+
}
|
|
8
|
+
export function isToolEnabledIn(table, name, profile) {
|
|
9
|
+
const { hosted } = toolPolicyFor(table, name);
|
|
10
|
+
return profile === "full" || hosted;
|
|
11
|
+
}
|
|
12
|
+
export function toolAnnotationsFor(table, name) {
|
|
13
|
+
const toolClass = toolPolicyFor(table, name).class;
|
|
14
|
+
return { readOnlyHint: toolClass === "read", destructiveHint: toolClass === "sensitive" };
|
|
15
|
+
}
|
|
16
|
+
/** Tool names in a category, in table order. */
|
|
17
|
+
export function toolsInCategoryOf(table, category) {
|
|
18
|
+
return Object.keys(table).filter((name) => table[name].category === category);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A stand-in for McpServer that every register*Tools(server, client) function can be
|
|
22
|
+
* handed. It is the one place a tool is filtered by profile and given its annotations.
|
|
23
|
+
*/
|
|
24
|
+
export function createPolicyRegistrar(server, table, profile) {
|
|
25
|
+
const registerAnnotated = server.tool.bind(server);
|
|
26
|
+
return {
|
|
27
|
+
tool(name, description, schema, handler) {
|
|
28
|
+
if (!isToolEnabledIn(table, name, profile))
|
|
29
|
+
return;
|
|
30
|
+
registerAnnotated(name, description, schema, toolAnnotationsFor(table, name), handler);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { DropspaceClient } from "./client.js";
|
|
3
|
+
import { type ToolProfile } from "./policy.js";
|
|
4
|
+
export interface CreateDropspaceServerOptions {
|
|
5
|
+
client: DropspaceClient;
|
|
6
|
+
version: string;
|
|
7
|
+
/** "full" serves every tool (stdio). "hosted" serves the tools marked hosted in tool-policy.ts. */
|
|
8
|
+
profile: ToolProfile;
|
|
9
|
+
}
|
|
10
|
+
export declare function createDropspaceServer({ client, version, profile }: CreateDropspaceServerOptions): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { createPolicyRegistrar } from "./policy.js";
|
|
3
|
+
import { TOOL_POLICY } from "./tool-policy.js";
|
|
4
|
+
import { registerLaunchTools } from "./tools/launches.js";
|
|
5
|
+
import { registerPersonaTools } from "./tools/personas.js";
|
|
6
|
+
import { registerConnectionTools } from "./tools/connections.js";
|
|
7
|
+
import { registerKeyTools } from "./tools/keys.js";
|
|
8
|
+
import { registerWebhookTools } from "./tools/webhooks.js";
|
|
9
|
+
import { registerDropspaceTools } from "./tools/dropspace.js";
|
|
10
|
+
import { registerUsageTools } from "./tools/usage.js";
|
|
11
|
+
import { registerCreditTools } from "./tools/credits.js";
|
|
12
|
+
import { registerMediaTools } from "./tools/media.js";
|
|
13
|
+
import { registerBoostTools } from "./tools/boosts.js";
|
|
14
|
+
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
15
|
+
import { registerRecommendationTools } from "./tools/recommendations.js";
|
|
16
|
+
import { registerRedditTools } from "./tools/reddit.js";
|
|
17
|
+
import { registerAudienceTools } from "./tools/audience.js";
|
|
18
|
+
export function createDropspaceServer({ client, version, profile }) {
|
|
19
|
+
const server = new McpServer({ name: "dropspace", version });
|
|
20
|
+
const registrar = createPolicyRegistrar(server, TOOL_POLICY, profile);
|
|
21
|
+
registerLaunchTools(registrar, client);
|
|
22
|
+
registerPersonaTools(registrar, client);
|
|
23
|
+
registerConnectionTools(registrar, client);
|
|
24
|
+
registerKeyTools(registrar, client);
|
|
25
|
+
registerWebhookTools(registrar, client);
|
|
26
|
+
registerDropspaceTools(registrar, client);
|
|
27
|
+
registerUsageTools(registrar, client);
|
|
28
|
+
registerCreditTools(registrar, client);
|
|
29
|
+
registerMediaTools(registrar, client);
|
|
30
|
+
registerBoostTools(registrar, client);
|
|
31
|
+
registerAutopilotTools(registrar, client);
|
|
32
|
+
registerRecommendationTools(registrar, client);
|
|
33
|
+
registerRedditTools(registrar, client);
|
|
34
|
+
registerAudienceTools(registrar, client);
|
|
35
|
+
return server;
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a tool is classified.
|
|
3
|
+
*
|
|
4
|
+
* classes are defined in policy.ts; this file only assigns them.
|
|
5
|
+
*
|
|
6
|
+
* hosted: whether the hosted endpoint (https://api.dropspace.dev/mcp) serves the tool.
|
|
7
|
+
* The stdio server serves every tool. Left off hosted: credits and boosts (payments and
|
|
8
|
+
* ad spend), API keys and webhooks (developer plumbing and credentials), and reddit
|
|
9
|
+
* engagement.
|
|
10
|
+
*
|
|
11
|
+
* category: the documentation grouping. Every tool count and category list in the docs
|
|
12
|
+
* page, llms.txt, and llms-full.txt is computed from this table.
|
|
13
|
+
*
|
|
14
|
+
* Registering a tool with no entry here throws, and __tests__/server.test.ts fails on
|
|
15
|
+
* any mismatch between this table and what the package registers.
|
|
16
|
+
*/
|
|
17
|
+
import { type PolicyTable, type ToolClass, type ToolPolicy, type ToolProfile } from "./policy.js";
|
|
18
|
+
export type { ToolClass, ToolPolicy, ToolProfile };
|
|
19
|
+
export declare const TOOL_CATEGORIES: readonly ["launches", "posts", "personas", "media", "connections", "boosts", "credits", "API keys", "webhooks", "dropspace", "usage", "autopilot", "recommendations", "reddit engagement", "audience"];
|
|
20
|
+
export type ToolCategory = (typeof TOOL_CATEGORIES)[number];
|
|
21
|
+
export declare const TOOL_POLICY: PolicyTable<ToolCategory>;
|
|
22
|
+
export declare const toolPolicy: (name: string) => ToolPolicy<"launches" | "personas" | "connections" | "webhooks" | "usage" | "credits" | "media" | "boosts" | "autopilot" | "recommendations" | "audience" | "dropspace" | "posts" | "API keys" | "reddit engagement">;
|
|
23
|
+
export declare const isToolEnabled: (name: string, profile: ToolProfile) => boolean;
|
|
24
|
+
export declare const toolAnnotations: (name: string) => {
|
|
25
|
+
readOnlyHint: boolean;
|
|
26
|
+
destructiveHint: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare const toolsInCategory: (category: ToolCategory) => string[];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a tool is classified.
|
|
3
|
+
*
|
|
4
|
+
* classes are defined in policy.ts; this file only assigns them.
|
|
5
|
+
*
|
|
6
|
+
* hosted: whether the hosted endpoint (https://api.dropspace.dev/mcp) serves the tool.
|
|
7
|
+
* The stdio server serves every tool. Left off hosted: credits and boosts (payments and
|
|
8
|
+
* ad spend), API keys and webhooks (developer plumbing and credentials), and reddit
|
|
9
|
+
* engagement.
|
|
10
|
+
*
|
|
11
|
+
* category: the documentation grouping. Every tool count and category list in the docs
|
|
12
|
+
* page, llms.txt, and llms-full.txt is computed from this table.
|
|
13
|
+
*
|
|
14
|
+
* Registering a tool with no entry here throws, and __tests__/server.test.ts fails on
|
|
15
|
+
* any mismatch between this table and what the package registers.
|
|
16
|
+
*/
|
|
17
|
+
import { isToolEnabledIn, toolAnnotationsFor, toolPolicyFor, toolsInCategoryOf, } from "./policy.js";
|
|
18
|
+
export const TOOL_CATEGORIES = [
|
|
19
|
+
"launches",
|
|
20
|
+
"posts",
|
|
21
|
+
"personas",
|
|
22
|
+
"media",
|
|
23
|
+
"connections",
|
|
24
|
+
"boosts",
|
|
25
|
+
"credits",
|
|
26
|
+
"API keys",
|
|
27
|
+
"webhooks",
|
|
28
|
+
"dropspace",
|
|
29
|
+
"usage",
|
|
30
|
+
"autopilot",
|
|
31
|
+
"recommendations",
|
|
32
|
+
"reddit engagement",
|
|
33
|
+
"audience",
|
|
34
|
+
];
|
|
35
|
+
const hosted = (category, toolClass) => ({
|
|
36
|
+
class: toolClass,
|
|
37
|
+
hosted: true,
|
|
38
|
+
category,
|
|
39
|
+
});
|
|
40
|
+
const localOnly = (category, toolClass) => ({
|
|
41
|
+
class: toolClass,
|
|
42
|
+
hosted: false,
|
|
43
|
+
category,
|
|
44
|
+
});
|
|
45
|
+
export const TOOL_POLICY = {
|
|
46
|
+
// launches
|
|
47
|
+
list_launches: hosted("launches", "read"),
|
|
48
|
+
get_launch: hosted("launches", "read"),
|
|
49
|
+
get_launch_status: hosted("launches", "read"),
|
|
50
|
+
get_launch_analytics: hosted("launches", "read"),
|
|
51
|
+
get_batch_launch_analytics: hosted("launches", "read"),
|
|
52
|
+
// create and update accept `publish` / `scheduled_date`, so either can put a post out
|
|
53
|
+
create_launch: hosted("launches", "sensitive"),
|
|
54
|
+
update_launch: hosted("launches", "sensitive"),
|
|
55
|
+
// a soft delete with no user-facing restore
|
|
56
|
+
delete_launch: hosted("launches", "sensitive"),
|
|
57
|
+
generate_content: hosted("launches", "write"),
|
|
58
|
+
retry_launch_content: hosted("launches", "write"),
|
|
59
|
+
publish_launch: hosted("launches", "sensitive"),
|
|
60
|
+
retry_launch: hosted("launches", "sensitive"),
|
|
61
|
+
// posts
|
|
62
|
+
delete_post: hosted("posts", "sensitive"),
|
|
63
|
+
delete_all_posts: hosted("posts", "sensitive"),
|
|
64
|
+
// personas
|
|
65
|
+
list_personas: hosted("personas", "read"),
|
|
66
|
+
get_persona: hosted("personas", "read"),
|
|
67
|
+
create_persona: hosted("personas", "write"),
|
|
68
|
+
update_persona: hosted("personas", "write"),
|
|
69
|
+
// a soft delete with no user-facing restore
|
|
70
|
+
delete_persona: hosted("personas", "sensitive"),
|
|
71
|
+
analyze_persona: hosted("personas", "write"),
|
|
72
|
+
finalize_persona: hosted("personas", "write"),
|
|
73
|
+
discard_persona_analysis: hosted("personas", "write"),
|
|
74
|
+
// media
|
|
75
|
+
generate_media: hosted("media", "write"),
|
|
76
|
+
get_media_job: hosted("media", "read"),
|
|
77
|
+
// connections
|
|
78
|
+
list_connections: hosted("connections", "read"),
|
|
79
|
+
// edits the profile ON the platform (youtube channel description)
|
|
80
|
+
update_connection_profile: hosted("connections", "sensitive"),
|
|
81
|
+
// autopilot
|
|
82
|
+
get_autopilot: hosted("autopilot", "read"),
|
|
83
|
+
list_autopilot_review: hosted("autopilot", "read"),
|
|
84
|
+
list_autopilot_runs: hosted("autopilot", "read"),
|
|
85
|
+
// deletes the draft launch with no restore
|
|
86
|
+
reject_autopilot_draft: hosted("autopilot", "sensitive"),
|
|
87
|
+
// enabling autopilot leads to posts with no further per-post step
|
|
88
|
+
configure_autopilot: hosted("autopilot", "sensitive"),
|
|
89
|
+
approve_autopilot_draft: hosted("autopilot", "sensitive"),
|
|
90
|
+
run_autopilot_now: hosted("autopilot", "sensitive"),
|
|
91
|
+
// recommendations
|
|
92
|
+
list_recommendations: hosted("recommendations", "read"),
|
|
93
|
+
// creates a DRAFT launch or a PAUSED boost; nothing publishes or spends
|
|
94
|
+
apply_recommendation: hosted("recommendations", "write"),
|
|
95
|
+
dismiss_recommendation: hosted("recommendations", "write"),
|
|
96
|
+
// audience, usage, status
|
|
97
|
+
get_audience: hosted("audience", "read"),
|
|
98
|
+
get_usage: hosted("usage", "read"),
|
|
99
|
+
get_changelog: hosted("usage", "read"),
|
|
100
|
+
get_dropspace_status: hosted("dropspace", "read"),
|
|
101
|
+
// reddit engagement
|
|
102
|
+
suggest_subreddits: localOnly("reddit engagement", "read"),
|
|
103
|
+
get_engagement_config: localOnly("reddit engagement", "read"),
|
|
104
|
+
get_engagement_standing: localOnly("reddit engagement", "read"),
|
|
105
|
+
list_engagement_comments: localOnly("reddit engagement", "read"),
|
|
106
|
+
update_engagement_config: localOnly("reddit engagement", "write"),
|
|
107
|
+
discover_engagement: localOnly("reddit engagement", "write"),
|
|
108
|
+
reject_engagement_comment: localOnly("reddit engagement", "write"),
|
|
109
|
+
activate_engagement_subreddit: localOnly("reddit engagement", "write"),
|
|
110
|
+
deactivate_engagement_subreddit: localOnly("reddit engagement", "write"),
|
|
111
|
+
approve_engagement_comment: localOnly("reddit engagement", "sensitive"),
|
|
112
|
+
// credits
|
|
113
|
+
get_credit_balance: localOnly("credits", "read"),
|
|
114
|
+
purchase_credit_pack: localOnly("credits", "sensitive"),
|
|
115
|
+
// boosts
|
|
116
|
+
list_boosts: localOnly("boosts", "read"),
|
|
117
|
+
get_boost: localOnly("boosts", "read"),
|
|
118
|
+
get_boost_performance: localOnly("boosts", "read"),
|
|
119
|
+
create_boost: localOnly("boosts", "write"),
|
|
120
|
+
activate_boost: localOnly("boosts", "sensitive"),
|
|
121
|
+
cancel_boost: localOnly("boosts", "sensitive"),
|
|
122
|
+
// api keys
|
|
123
|
+
get_current_api_key: localOnly("API keys", "read"),
|
|
124
|
+
list_api_keys: localOnly("API keys", "read"),
|
|
125
|
+
rename_api_key: localOnly("API keys", "write"),
|
|
126
|
+
create_api_key: localOnly("API keys", "sensitive"),
|
|
127
|
+
revoke_api_key: localOnly("API keys", "sensitive"),
|
|
128
|
+
rotate_api_key: localOnly("API keys", "sensitive"),
|
|
129
|
+
// webhooks
|
|
130
|
+
list_webhooks: localOnly("webhooks", "read"),
|
|
131
|
+
get_webhook: localOnly("webhooks", "read"),
|
|
132
|
+
list_webhook_deliveries: localOnly("webhooks", "read"),
|
|
133
|
+
create_webhook: localOnly("webhooks", "write"),
|
|
134
|
+
update_webhook: localOnly("webhooks", "write"),
|
|
135
|
+
// a hard delete with no restore
|
|
136
|
+
delete_webhook: localOnly("webhooks", "sensitive"),
|
|
137
|
+
test_webhook: localOnly("webhooks", "write"),
|
|
138
|
+
rotate_webhook_secret: localOnly("webhooks", "sensitive"),
|
|
139
|
+
};
|
|
140
|
+
export const toolPolicy = (name) => toolPolicyFor(TOOL_POLICY, name);
|
|
141
|
+
export const isToolEnabled = (name, profile) => isToolEnabledIn(TOOL_POLICY, name, profile);
|
|
142
|
+
export const toolAnnotations = (name) => toolAnnotationsFor(TOOL_POLICY, name);
|
|
143
|
+
export const toolsInCategory = (category) => toolsInCategoryOf(TOOL_POLICY, category);
|
package/dist/tools/launches.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { contentIdempotencyKey } from "../idempotency.js";
|
|
2
3
|
// 11 enum values but only 10 distinct platforms — linkedin-community is a LinkedIn variant, not a separate platform.
|
|
3
4
|
// auto-post (7): facebook, linkedin, twitter, reddit, instagram, tiktok, youtube
|
|
4
5
|
// guided (3): substack, producthunt, hackernews
|
|
@@ -161,9 +162,20 @@ export function registerLaunchTools(server, client) {
|
|
|
161
162
|
};
|
|
162
163
|
}
|
|
163
164
|
try {
|
|
164
|
-
|
|
165
|
+
// A retry after a timeout (by an agent, or a person pressing "try again") must not
|
|
166
|
+
// create a second launch: the key is derived from the arguments, so the API replays
|
|
167
|
+
// the launch it already created for them.
|
|
168
|
+
const { result, replayed } = await client.postIdempotent("/launches", args, await contentIdempotencyKey(args));
|
|
169
|
+
const text = JSON.stringify(result, null, 2);
|
|
165
170
|
return {
|
|
166
|
-
content: [
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: replayed
|
|
175
|
+
? `note: an identical launch was created within the last 24 hours, so dropspace returned it - no new launch was created.\n${text}`
|
|
176
|
+
: text,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
167
179
|
};
|
|
168
180
|
}
|
|
169
181
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jclvsh/dropspace",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.1",
|
|
4
4
|
"description": "MCP server and CLI for the Dropspace API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,6 +10,25 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./hosted": {
|
|
19
|
+
"types": "./dist/hosted.d.ts",
|
|
20
|
+
"default": "./dist/hosted.js"
|
|
21
|
+
},
|
|
22
|
+
"./policy": {
|
|
23
|
+
"types": "./dist/policy.d.ts",
|
|
24
|
+
"default": "./dist/policy.js"
|
|
25
|
+
},
|
|
26
|
+
"./tool-policy": {
|
|
27
|
+
"types": "./dist/tool-policy.d.ts",
|
|
28
|
+
"default": "./dist/tool-policy.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
13
32
|
"bin": {
|
|
14
33
|
"dropspace-mcp": "dist/index.js",
|
|
15
34
|
"dropspace": "dist/cli.js"
|
|
@@ -20,17 +39,21 @@
|
|
|
20
39
|
],
|
|
21
40
|
"scripts": {
|
|
22
41
|
"build": "tsc",
|
|
42
|
+
"prepare": "tsc",
|
|
23
43
|
"prepublishOnly": "npm run build",
|
|
24
|
-
"start": "node dist/index.js"
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
46
|
+
"test": "vitest run"
|
|
25
47
|
},
|
|
26
48
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
28
50
|
"commander": "^14.0.3",
|
|
29
51
|
"zod": "^3.24.0"
|
|
30
52
|
},
|
|
31
53
|
"devDependencies": {
|
|
32
54
|
"@types/node": "^22.0.0",
|
|
33
|
-
"typescript": "^5.7.0"
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^4.0.18"
|
|
34
57
|
},
|
|
35
58
|
"engines": {
|
|
36
59
|
"node": ">=18"
|