@jclvsh/iris 0.22.0 → 0.23.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 +2 -0
- package/dist/client.d.ts +21 -1
- package/dist/client.js +54 -14
- package/dist/commands/campaigns.js +1 -1
- 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 -39
- package/dist/policy.d.ts +36 -0
- package/dist/policy.js +33 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +42 -0
- package/dist/tool-policy.d.ts +12 -0
- package/dist/tool-policy.js +152 -0
- package/dist/tools/campaigns.js +17 -3
- package/dist/types.d.ts +10 -6
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Or run one-shot without installing:
|
|
|
19
19
|
npx -y -p @jclvsh/iris iris-mcp
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
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.
|
|
23
|
+
|
|
22
24
|
## CLI
|
|
23
25
|
|
|
24
26
|
```bash
|
package/dist/client.d.ts
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
+
export interface IrisClientOptions {
|
|
2
|
+
/** API key to send. Defaults to the IRIS_API_KEY environment variable. */
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
/** REST base URL. Defaults to IRIS_API_URL, then https://api.iris-ai.dev. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
1
7
|
export declare class IrisClient {
|
|
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?: IrisClientOptions);
|
|
5
13
|
get<T>(path: string, params?: Record<string, string>): Promise<T>;
|
|
6
14
|
post<T>(path: string, body?: unknown): Promise<T>;
|
|
7
15
|
patch<T>(path: string, body: unknown): Promise<T>;
|
|
8
16
|
delete(path: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* POST with an Idempotency-Key. The API answers a repeat of the same key within 24 hours
|
|
19
|
+
* with the original response and `Idempotent-Replayed: true`; `replayed` surfaces that so
|
|
20
|
+
* the caller can say "nothing new was created".
|
|
21
|
+
*/
|
|
22
|
+
postIdempotent<T>(path: string, body: unknown, idempotencyKey: string): Promise<{
|
|
23
|
+
result: T;
|
|
24
|
+
replayed: boolean;
|
|
25
|
+
}>;
|
|
9
26
|
private request;
|
|
27
|
+
/** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
|
|
28
|
+
private readJson;
|
|
29
|
+
private send;
|
|
10
30
|
private handleError;
|
|
11
31
|
}
|
package/dist/client.js
CHANGED
|
@@ -30,23 +30,28 @@ function safeStringify(value) {
|
|
|
30
30
|
export class IrisClient {
|
|
31
31
|
apiKey;
|
|
32
32
|
baseUrl;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/** True when the base URL was injected; the network-error hint then names no URL or env var. */
|
|
34
|
+
hasInjectedBaseUrl;
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
const apiKey = options.apiKey ?? process.env.IRIS_API_KEY;
|
|
35
37
|
if (!apiKey) {
|
|
36
38
|
throw new Error("IRIS_API_KEY environment variable is required. " +
|
|
37
39
|
"Get your API key from https://iris-ai.dev/settings/api-keys");
|
|
38
40
|
}
|
|
39
41
|
if (!apiKey.startsWith("is_live_")) {
|
|
40
|
-
|
|
42
|
+
// An injected key came from a hosted caller, who never set the env var.
|
|
43
|
+
const subject = options.apiKey !== undefined ? "api key" : "IRIS_API_KEY";
|
|
44
|
+
throw new Error(`${subject} must start with 'is_live_'. ` +
|
|
41
45
|
"Get a valid API key from https://iris-ai.dev/settings/api-keys");
|
|
42
46
|
}
|
|
43
47
|
this.apiKey = apiKey;
|
|
48
|
+
this.hasInjectedBaseUrl = Boolean(options.baseUrl);
|
|
44
49
|
// Default to api.iris-ai.dev (Stripe/GitHub/OpenAI convention). The api host
|
|
45
50
|
// rewrites /<resource> → /api/v1/<resource> via middleware (see iris/src/proxy.ts),
|
|
46
51
|
// so callers no longer pass the /api/v1/ prefix. Override via IRIS_API_URL if needed.
|
|
47
52
|
// Older versions (≤0.5.5) defaulted to https://www.iris-ai.dev/api/v1 — that host
|
|
48
53
|
// still responds, but the api host is canonical going forward.
|
|
49
|
-
this.baseUrl = (process.env.IRIS_API_URL || "https://api.iris-ai.dev").replace(/\/+$/, "");
|
|
54
|
+
this.baseUrl = (options.baseUrl || process.env.IRIS_API_URL || "https://api.iris-ai.dev").replace(/\/+$/, "");
|
|
50
55
|
}
|
|
51
56
|
async get(path, params) {
|
|
52
57
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
@@ -76,33 +81,68 @@ export class IrisClient {
|
|
|
76
81
|
async delete(path) {
|
|
77
82
|
await this.request(`${this.baseUrl}${path}`, { method: "DELETE" });
|
|
78
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* POST with an Idempotency-Key. The API answers a repeat of the same key within 24 hours
|
|
86
|
+
* with the original response and `Idempotent-Replayed: true`; `replayed` surfaces that so
|
|
87
|
+
* the caller can say "nothing new was created".
|
|
88
|
+
*/
|
|
89
|
+
async postIdempotent(path, body, idempotencyKey) {
|
|
90
|
+
const response = await this.send(`${this.baseUrl}${path}`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
result: await this.readJson(response),
|
|
97
|
+
replayed: response.headers.get("Idempotent-Replayed") === "true",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
79
100
|
async request(url, init) {
|
|
101
|
+
const response = await this.send(url, init);
|
|
102
|
+
if (response.status === 204) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return this.readJson(response);
|
|
106
|
+
}
|
|
107
|
+
/** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
|
|
108
|
+
async readJson(response) {
|
|
109
|
+
try {
|
|
110
|
+
return (await response.json());
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
114
|
+
throw new Error("request timed out after 30 seconds");
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async send(url, init) {
|
|
120
|
+
let response;
|
|
80
121
|
try {
|
|
81
|
-
|
|
122
|
+
response = await fetch(url, {
|
|
82
123
|
...init,
|
|
83
124
|
headers: {
|
|
84
|
-
...init.headers,
|
|
125
|
+
...(init.headers || {}),
|
|
85
126
|
Authorization: `Bearer ${this.apiKey}`,
|
|
86
127
|
},
|
|
87
128
|
signal: AbortSignal.timeout(30_000),
|
|
88
129
|
});
|
|
89
|
-
if (!response.ok && response.status !== 204) {
|
|
90
|
-
await this.handleError(response);
|
|
91
|
-
}
|
|
92
|
-
if (response.status === 204) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
return (await response.json());
|
|
96
130
|
}
|
|
97
131
|
catch (error) {
|
|
98
132
|
if (error instanceof Error && error.name === "TimeoutError") {
|
|
99
133
|
throw new Error("request timed out after 30 seconds");
|
|
100
134
|
}
|
|
101
135
|
if (error instanceof TypeError) {
|
|
102
|
-
throw new Error(
|
|
136
|
+
throw new Error(this.hasInjectedBaseUrl
|
|
137
|
+
? `network error reaching the api: ${error.message}`
|
|
138
|
+
: `network error: ${error.message}. check IRIS_API_URL and your connection`);
|
|
103
139
|
}
|
|
104
140
|
throw error;
|
|
105
141
|
}
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
await this.handleError(response);
|
|
144
|
+
}
|
|
145
|
+
return response;
|
|
106
146
|
}
|
|
107
147
|
async handleError(response) {
|
|
108
148
|
let message;
|
|
@@ -110,7 +110,7 @@ export function registerCampaignCommands(program, client) {
|
|
|
110
110
|
});
|
|
111
111
|
campaigns
|
|
112
112
|
.command("analyze <id>")
|
|
113
|
-
.description("
|
|
113
|
+
.description("queue AI analysis of a campaign to generate target profiles (from its product, or from its goal)")
|
|
114
114
|
.action(async (id) => {
|
|
115
115
|
try {
|
|
116
116
|
const result = await client.post(`/campaigns/${id}/analyze`);
|
|
@@ -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 iris API key. The host authenticates it before calling in. */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** REST base URL the tools call, e.g. https://api.iris-ai.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 { IrisClient } from "./client.js";
|
|
2
|
+
import { serveStatelessMcp } from "./hosted-transport.js";
|
|
3
|
+
import { createIrisServer } 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 IrisClient({ apiKey: options.apiKey, baseUrl: options.baseUrl });
|
|
7
|
+
const server = createIrisServer({ 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,31 +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 { IrisClient } from "./client.js";
|
|
6
|
-
import {
|
|
7
|
-
import { registerContactTools } from "./tools/contacts.js";
|
|
8
|
-
import { registerConversationTools } from "./tools/conversations.js";
|
|
9
|
-
import { registerMessageTools } from "./tools/messages.js";
|
|
10
|
-
import { registerAiTools } from "./tools/ai.js";
|
|
11
|
-
import { registerDiscoveryTools } from "./tools/discovery.js";
|
|
12
|
-
import { registerAnalyticsTools } from "./tools/analytics.js";
|
|
13
|
-
import { registerBillingTools } from "./tools/billing.js";
|
|
14
|
-
import { registerChannelTools } from "./tools/channels.js";
|
|
15
|
-
import { registerNotificationTools } from "./tools/notifications.js";
|
|
16
|
-
import { registerKeyTools } from "./tools/keys.js";
|
|
17
|
-
import { registerWebhookTools } from "./tools/webhooks.js";
|
|
18
|
-
import { registerAgentCreditTools } from "./tools/agent-credits.js";
|
|
19
|
-
import { registerUsageTools } from "./tools/usage.js";
|
|
20
|
-
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
21
|
-
import { registerResponderTools } from "./tools/responder.js";
|
|
22
|
-
import { registerAudienceTools } from "./tools/audiences.js";
|
|
5
|
+
import { createIrisServer } from "./server.js";
|
|
23
6
|
const requireFromHere = createRequire(import.meta.url);
|
|
24
7
|
const pkg = requireFromHere("../package.json");
|
|
25
|
-
const server = new McpServer({
|
|
26
|
-
name: "iris",
|
|
27
|
-
version: pkg.version,
|
|
28
|
-
});
|
|
29
8
|
// Lazy client: defer the IrisClient constructor (which throws when
|
|
30
9
|
// IRIS_API_KEY is missing) until a tool invocation actually issues an HTTP call.
|
|
31
10
|
// MCP discovery (initialize, tools/list) never touches the proxy, so the server
|
|
@@ -40,22 +19,6 @@ const client = new Proxy({}, {
|
|
|
40
19
|
return typeof value === "function" ? value.bind(realClient) : value;
|
|
41
20
|
},
|
|
42
21
|
});
|
|
43
|
-
|
|
44
|
-
registerContactTools(server, client);
|
|
45
|
-
registerConversationTools(server, client);
|
|
46
|
-
registerMessageTools(server, client);
|
|
47
|
-
registerAiTools(server, client);
|
|
48
|
-
registerDiscoveryTools(server, client);
|
|
49
|
-
registerAnalyticsTools(server, client);
|
|
50
|
-
registerBillingTools(server, client);
|
|
51
|
-
registerChannelTools(server, client);
|
|
52
|
-
registerNotificationTools(server, client);
|
|
53
|
-
registerKeyTools(server, client);
|
|
54
|
-
registerWebhookTools(server, client);
|
|
55
|
-
registerAgentCreditTools(server, client);
|
|
56
|
-
registerUsageTools(server, client);
|
|
57
|
-
registerAutopilotTools(server, client);
|
|
58
|
-
registerResponderTools(server, client);
|
|
59
|
-
registerAudienceTools(server, client);
|
|
22
|
+
const server = createIrisServer({ client, version: pkg.version, profile: "full" });
|
|
60
23
|
const transport = new StdioServerTransport();
|
|
61
24
|
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 { IrisClient } from "./client.js";
|
|
3
|
+
import { type ToolProfile } from "./policy.js";
|
|
4
|
+
export interface CreateIrisServerOptions {
|
|
5
|
+
client: IrisClient;
|
|
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 createIrisServer({ client, version, profile }: CreateIrisServerOptions): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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 { registerCampaignTools } from "./tools/campaigns.js";
|
|
5
|
+
import { registerContactTools } from "./tools/contacts.js";
|
|
6
|
+
import { registerConversationTools } from "./tools/conversations.js";
|
|
7
|
+
import { registerMessageTools } from "./tools/messages.js";
|
|
8
|
+
import { registerAiTools } from "./tools/ai.js";
|
|
9
|
+
import { registerDiscoveryTools } from "./tools/discovery.js";
|
|
10
|
+
import { registerAnalyticsTools } from "./tools/analytics.js";
|
|
11
|
+
import { registerBillingTools } from "./tools/billing.js";
|
|
12
|
+
import { registerChannelTools } from "./tools/channels.js";
|
|
13
|
+
import { registerNotificationTools } from "./tools/notifications.js";
|
|
14
|
+
import { registerKeyTools } from "./tools/keys.js";
|
|
15
|
+
import { registerWebhookTools } from "./tools/webhooks.js";
|
|
16
|
+
import { registerAgentCreditTools } from "./tools/agent-credits.js";
|
|
17
|
+
import { registerUsageTools } from "./tools/usage.js";
|
|
18
|
+
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
19
|
+
import { registerResponderTools } from "./tools/responder.js";
|
|
20
|
+
import { registerAudienceTools } from "./tools/audiences.js";
|
|
21
|
+
export function createIrisServer({ client, version, profile }) {
|
|
22
|
+
const server = new McpServer({ name: "iris", version });
|
|
23
|
+
const registrar = createPolicyRegistrar(server, TOOL_POLICY, profile);
|
|
24
|
+
registerCampaignTools(registrar, client);
|
|
25
|
+
registerContactTools(registrar, client);
|
|
26
|
+
registerConversationTools(registrar, client);
|
|
27
|
+
registerMessageTools(registrar, client);
|
|
28
|
+
registerAiTools(registrar, client);
|
|
29
|
+
registerDiscoveryTools(registrar, client);
|
|
30
|
+
registerAnalyticsTools(registrar, client);
|
|
31
|
+
registerBillingTools(registrar, client);
|
|
32
|
+
registerChannelTools(registrar, client);
|
|
33
|
+
registerNotificationTools(registrar, client);
|
|
34
|
+
registerKeyTools(registrar, client);
|
|
35
|
+
registerWebhookTools(registrar, client);
|
|
36
|
+
registerAgentCreditTools(registrar, client);
|
|
37
|
+
registerUsageTools(registrar, client);
|
|
38
|
+
registerAutopilotTools(registrar, client);
|
|
39
|
+
registerResponderTools(registrar, client);
|
|
40
|
+
registerAudienceTools(registrar, client);
|
|
41
|
+
return server;
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type PolicyTable, type ToolClass, type ToolPolicy, type ToolProfile } from "./policy.js";
|
|
2
|
+
export type { ToolClass, ToolPolicy, ToolProfile };
|
|
3
|
+
export declare const TOOL_CATEGORIES: readonly ["campaigns", "contacts", "conversations", "messages", "channels", "discovery", "audiences", "autopilot", "responder", "ai", "analytics", "notifications", "usage", "billing", "credits", "API keys", "webhooks"];
|
|
4
|
+
export type ToolCategory = (typeof TOOL_CATEGORIES)[number];
|
|
5
|
+
export declare const TOOL_POLICY: PolicyTable<ToolCategory>;
|
|
6
|
+
export declare const toolPolicy: (name: string) => ToolPolicy<"campaigns" | "contacts" | "usage" | "messages" | "conversations" | "discovery" | "ai" | "webhooks" | "notifications" | "analytics" | "billing" | "channels" | "credits" | "responder" | "audiences" | "autopilot" | "API keys">;
|
|
7
|
+
export declare const isToolEnabled: (name: string, profile: ToolProfile) => boolean;
|
|
8
|
+
export declare const toolAnnotations: (name: string) => {
|
|
9
|
+
readOnlyHint: boolean;
|
|
10
|
+
destructiveHint: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const toolsInCategory: (category: ToolCategory) => string[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { isToolEnabledIn, toolAnnotationsFor, toolPolicyFor, toolsInCategoryOf, } from "./policy.js";
|
|
2
|
+
export const TOOL_CATEGORIES = [
|
|
3
|
+
"campaigns",
|
|
4
|
+
"contacts",
|
|
5
|
+
"conversations",
|
|
6
|
+
"messages",
|
|
7
|
+
"channels",
|
|
8
|
+
"discovery",
|
|
9
|
+
"audiences",
|
|
10
|
+
"autopilot",
|
|
11
|
+
"responder",
|
|
12
|
+
"ai",
|
|
13
|
+
"analytics",
|
|
14
|
+
"notifications",
|
|
15
|
+
"usage",
|
|
16
|
+
"billing",
|
|
17
|
+
"credits",
|
|
18
|
+
"API keys",
|
|
19
|
+
"webhooks",
|
|
20
|
+
];
|
|
21
|
+
const hosted = (category, toolClass) => ({
|
|
22
|
+
class: toolClass,
|
|
23
|
+
hosted: true,
|
|
24
|
+
category,
|
|
25
|
+
});
|
|
26
|
+
const localOnly = (category, toolClass) => ({
|
|
27
|
+
class: toolClass,
|
|
28
|
+
hosted: false,
|
|
29
|
+
category,
|
|
30
|
+
});
|
|
31
|
+
export const TOOL_POLICY = {
|
|
32
|
+
// campaigns
|
|
33
|
+
list_campaigns: hosted("campaigns", "read"),
|
|
34
|
+
get_campaign: hosted("campaigns", "read"),
|
|
35
|
+
list_campaign_contacts: hosted("campaigns", "read"),
|
|
36
|
+
list_campaign_chats: hosted("campaigns", "read"),
|
|
37
|
+
get_campaign_chat_messages: hosted("campaigns", "read"),
|
|
38
|
+
get_campaign_usage: hosted("campaigns", "read"),
|
|
39
|
+
list_target_profiles: hosted("campaigns", "read"),
|
|
40
|
+
create_campaign: hosted("campaigns", "write"),
|
|
41
|
+
update_campaign: hosted("campaigns", "write"),
|
|
42
|
+
add_campaign_contacts: hosted("campaigns", "write"),
|
|
43
|
+
rename_campaign_chat: hosted("campaigns", "write"),
|
|
44
|
+
analyze_campaign: hosted("campaigns", "write"),
|
|
45
|
+
create_target_profile: hosted("campaigns", "write"),
|
|
46
|
+
// soft deletes with no user-facing restore
|
|
47
|
+
delete_campaign: hosted("campaigns", "sensitive"),
|
|
48
|
+
delete_campaign_chat: hosted("campaigns", "sensitive"),
|
|
49
|
+
delete_target_profile: hosted("campaigns", "sensitive"),
|
|
50
|
+
// contacts
|
|
51
|
+
list_contacts: hosted("contacts", "read"),
|
|
52
|
+
get_contact: hosted("contacts", "read"),
|
|
53
|
+
get_contact_handles: hosted("contacts", "read"),
|
|
54
|
+
get_contact_usage: hosted("contacts", "read"),
|
|
55
|
+
list_contact_conversations: hosted("contacts", "read"),
|
|
56
|
+
create_contact: hosted("contacts", "write"),
|
|
57
|
+
update_contact: hosted("contacts", "write"),
|
|
58
|
+
bulk_import_contacts: hosted("contacts", "write"),
|
|
59
|
+
delete_contact: hosted("contacts", "sensitive"),
|
|
60
|
+
// conversations
|
|
61
|
+
list_conversations: hosted("conversations", "read"),
|
|
62
|
+
get_conversation: hosted("conversations", "read"),
|
|
63
|
+
get_conversation_messages: hosted("conversations", "read"),
|
|
64
|
+
get_engagement_metrics: hosted("conversations", "read"),
|
|
65
|
+
create_conversation: hosted("conversations", "write"),
|
|
66
|
+
mark_conversation_read: hosted("conversations", "write"),
|
|
67
|
+
update_conversation: hosted("conversations", "write"),
|
|
68
|
+
// records a message without transmitting it: any status other than pending_approval records it as sent
|
|
69
|
+
create_conversation_message: hosted("conversations", "sensitive"),
|
|
70
|
+
// messages
|
|
71
|
+
list_pending_messages: hosted("messages", "read"),
|
|
72
|
+
mark_message_sent: hosted("messages", "write"),
|
|
73
|
+
submit_message_feedback: hosted("messages", "write"),
|
|
74
|
+
// these send, or discard a draft with no restore
|
|
75
|
+
approve_message: hosted("messages", "sensitive"),
|
|
76
|
+
retry_message: hosted("messages", "sensitive"),
|
|
77
|
+
bulk_message_action: hosted("messages", "sensitive"),
|
|
78
|
+
reject_message: hosted("messages", "sensitive"),
|
|
79
|
+
// channels
|
|
80
|
+
list_connected_accounts: hosted("channels", "read"),
|
|
81
|
+
check_channel_availability: hosted("channels", "read"),
|
|
82
|
+
send_channel_message: hosted("channels", "sensitive"),
|
|
83
|
+
// discovery (spends quota and finds third parties; nothing is sent)
|
|
84
|
+
get_discovery_job: hosted("discovery", "read"),
|
|
85
|
+
list_discovery_sources: hosted("discovery", "read"),
|
|
86
|
+
search_contacts: hosted("discovery", "write"),
|
|
87
|
+
search_campaign_contacts: hosted("discovery", "write"),
|
|
88
|
+
// audiences
|
|
89
|
+
list_audiences: hosted("audiences", "read"),
|
|
90
|
+
create_audience: hosted("audiences", "write"),
|
|
91
|
+
generate_audiences: hosted("audiences", "write"),
|
|
92
|
+
attach_audience_to_campaign: hosted("audiences", "write"),
|
|
93
|
+
delete_audience: hosted("audiences", "sensitive"),
|
|
94
|
+
// autopilot (review-only: it drafts and discovers, it never sends)
|
|
95
|
+
get_autopilot_status: hosted("autopilot", "read"),
|
|
96
|
+
configure_autopilot: hosted("autopilot", "write"),
|
|
97
|
+
run_autopilot_now: hosted("autopilot", "write"),
|
|
98
|
+
// responder (auto-replies and comment-triggered DMs send without a per-message step)
|
|
99
|
+
get_responder: hosted("responder", "read"),
|
|
100
|
+
list_responder_activity: hosted("responder", "read"),
|
|
101
|
+
get_conversation_responder: hosted("responder", "read"),
|
|
102
|
+
list_responder_comment_triggers: hosted("responder", "read"),
|
|
103
|
+
list_responder_posts: hosted("responder", "read"),
|
|
104
|
+
configure_responder: hosted("responder", "sensitive"),
|
|
105
|
+
set_conversation_responder: hosted("responder", "sensitive"),
|
|
106
|
+
create_responder_comment_trigger: hosted("responder", "sensitive"),
|
|
107
|
+
update_responder_comment_trigger: hosted("responder", "sensitive"),
|
|
108
|
+
delete_responder_comment_trigger: hosted("responder", "sensitive"),
|
|
109
|
+
// ai
|
|
110
|
+
get_ai_usage: hosted("ai", "read"),
|
|
111
|
+
generate_content: hosted("ai", "write"),
|
|
112
|
+
generate_campaign_content: hosted("ai", "write"),
|
|
113
|
+
analyze_response: hosted("ai", "write"),
|
|
114
|
+
// analytics
|
|
115
|
+
get_analytics_overview: hosted("analytics", "read"),
|
|
116
|
+
get_campaign_analytics: hosted("analytics", "read"),
|
|
117
|
+
get_performance_metrics: hosted("analytics", "read"),
|
|
118
|
+
get_response_time: hosted("analytics", "read"),
|
|
119
|
+
get_contact_types: hosted("analytics", "read"),
|
|
120
|
+
// notifications
|
|
121
|
+
list_notifications: hosted("notifications", "read"),
|
|
122
|
+
update_notification: hosted("notifications", "write"),
|
|
123
|
+
mark_all_notifications_read: hosted("notifications", "write"),
|
|
124
|
+
delete_notification: hosted("notifications", "sensitive"),
|
|
125
|
+
// usage + billing
|
|
126
|
+
get_usage: hosted("usage", "read"),
|
|
127
|
+
get_changelog: hosted("usage", "read"),
|
|
128
|
+
get_billing_plans: hosted("billing", "read"),
|
|
129
|
+
// credits (agent payments: never hosted)
|
|
130
|
+
get_credit_balance: localOnly("credits", "read"),
|
|
131
|
+
purchase_credit_pack: localOnly("credits", "sensitive"),
|
|
132
|
+
// api keys (credentials: never hosted)
|
|
133
|
+
get_current_api_key: localOnly("API keys", "read"),
|
|
134
|
+
list_api_keys: localOnly("API keys", "read"),
|
|
135
|
+
rename_api_key: localOnly("API keys", "write"),
|
|
136
|
+
create_api_key: localOnly("API keys", "sensitive"),
|
|
137
|
+
revoke_api_key: localOnly("API keys", "sensitive"),
|
|
138
|
+
rotate_api_key: localOnly("API keys", "sensitive"),
|
|
139
|
+
// webhooks (developer plumbing: never hosted)
|
|
140
|
+
list_webhooks: localOnly("webhooks", "read"),
|
|
141
|
+
get_webhook: localOnly("webhooks", "read"),
|
|
142
|
+
list_webhook_deliveries: localOnly("webhooks", "read"),
|
|
143
|
+
create_webhook: localOnly("webhooks", "write"),
|
|
144
|
+
update_webhook: localOnly("webhooks", "write"),
|
|
145
|
+
test_webhook: localOnly("webhooks", "write"),
|
|
146
|
+
delete_webhook: localOnly("webhooks", "sensitive"),
|
|
147
|
+
rotate_webhook_secret: localOnly("webhooks", "sensitive"),
|
|
148
|
+
};
|
|
149
|
+
export const toolPolicy = (name) => toolPolicyFor(TOOL_POLICY, name);
|
|
150
|
+
export const isToolEnabled = (name, profile) => isToolEnabledIn(TOOL_POLICY, name, profile);
|
|
151
|
+
export const toolAnnotations = (name) => toolAnnotationsFor(TOOL_POLICY, name);
|
|
152
|
+
export const toolsInCategory = (category) => toolsInCategoryOf(TOOL_POLICY, category);
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatMcpError } from "../cli-utils.js";
|
|
3
|
+
import { contentIdempotencyKey } from "../idempotency.js";
|
|
3
4
|
const CAMPAIGN_STATUSES = ["draft", "active", "paused", "completed", "deleted"];
|
|
4
5
|
const OBJECTIVE_TYPES = ["venue_dj", "venue_host", "product_promotion", "custom"];
|
|
5
6
|
const TARGET_PROFILE_TYPES = [
|
|
@@ -46,8 +47,21 @@ export function registerCampaignTools(server, client) {
|
|
|
46
47
|
.describe("product description (for product_promotion objective)"),
|
|
47
48
|
}, async (args) => {
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// A retry after a timeout (by an agent, or a person pressing "try again") must not
|
|
51
|
+
// create a second campaign: the key is derived from the arguments, so the API replays
|
|
52
|
+
// the campaign it already created for them.
|
|
53
|
+
const { result, replayed } = await client.postIdempotent("/campaigns", args, await contentIdempotencyKey(args));
|
|
54
|
+
const text = JSON.stringify(result, null, 2);
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: replayed
|
|
60
|
+
? `note: an identical campaign was created within the last 24 hours, so iris returned it - no new campaign was created.\n${text}`
|
|
61
|
+
: text,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
51
65
|
}
|
|
52
66
|
catch (error) {
|
|
53
67
|
return formatMcpError(error);
|
|
@@ -193,7 +207,7 @@ export function registerCampaignTools(server, client) {
|
|
|
193
207
|
return formatMcpError(error);
|
|
194
208
|
}
|
|
195
209
|
});
|
|
196
|
-
server.tool("analyze_campaign", "
|
|
210
|
+
server.tool("analyze_campaign", "queue AI analysis of a campaign to generate its target profiles: from the product for product_promotion campaigns, from goal_description for any other campaign. runs asynchronously; poll get_campaign until analysis_status is completed, then search_campaign_contacts. requires write scope.", {
|
|
197
211
|
id: z.string().uuid().describe("campaign ID"),
|
|
198
212
|
}, async ({ id }) => {
|
|
199
213
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export interface Campaign {
|
|
|
33
33
|
follow_up_enabled: boolean;
|
|
34
34
|
follow_up_max_count: number;
|
|
35
35
|
follow_up_delay_days: number;
|
|
36
|
+
goal_description?: string | null;
|
|
37
|
+
analysis_status?: "none" | "pending" | "analyzing" | "completed" | "failed";
|
|
38
|
+
analysis_error?: string | null;
|
|
36
39
|
created_at: string;
|
|
37
40
|
updated_at: string;
|
|
38
41
|
}
|
|
@@ -43,14 +46,15 @@ export interface CampaignWithStats extends Campaign {
|
|
|
43
46
|
emails_opened?: number;
|
|
44
47
|
emails_replied?: number;
|
|
45
48
|
}
|
|
49
|
+
/** `GET /campaigns/{id}/usage` */
|
|
46
50
|
export interface CampaignUsage {
|
|
47
51
|
campaign_id: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
status: string;
|
|
53
|
+
total_contacts: number;
|
|
54
|
+
total_conversations: number;
|
|
55
|
+
delivery_status: Record<string, number>;
|
|
56
|
+
/** id of a contact search running for the campaign right now; null when none is */
|
|
57
|
+
discovery_job_id: string | null;
|
|
54
58
|
}
|
|
55
59
|
/** `POST /campaigns/{id}/contacts` */
|
|
56
60
|
export interface AddCampaignContactsResult {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jclvsh/iris",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "MCP server for the Iris 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
|
"iris-mcp": "dist/index.js",
|
|
15
34
|
"iris": "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": {
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
32
55
|
"typescript": "^5.7.0",
|
|
33
|
-
"
|
|
56
|
+
"vitest": "^4.0.18"
|
|
34
57
|
},
|
|
35
58
|
"engines": {
|
|
36
59
|
"node": ">=18"
|