@jclvsh/iris 0.10.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-utils.d.ts +5 -5
- package/dist/cli-utils.js +5 -5
- package/dist/commands/ai.js +4 -1
- package/dist/commands/conversations.js +39 -1
- package/dist/index.js +6 -0
- package/dist/tools/ai.js +29 -8
- package/dist/tools/audiences.d.ts +3 -0
- package/dist/tools/audiences.js +99 -0
- package/dist/tools/autopilot.d.ts +3 -0
- package/dist/tools/autopilot.js +70 -0
- package/dist/tools/conversations.js +44 -0
- package/dist/tools/keys.js +2 -2
- package/dist/tools/usage.d.ts +7 -0
- package/dist/tools/usage.js +16 -0
- package/dist/types.d.ts +51 -0
- package/package.json +2 -1
package/dist/cli-utils.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function formatOutput(data: unknown, format?: string): void;
|
|
11
11
|
/**
|
|
12
|
-
* Map
|
|
12
|
+
* Map a fleet error-code prefix to a CLI exit code.
|
|
13
13
|
*
|
|
14
14
|
* Convention so shell scripts can branch on failure type without parsing stderr:
|
|
15
15
|
* - 0: success (not handled here)
|
|
@@ -20,14 +20,14 @@ export declare function formatOutput(data: unknown, format?: string): void;
|
|
|
20
20
|
* - 5: payment / billing (PAYMENT_*, BILLING_*)
|
|
21
21
|
* - 6: not found (any *_NOT_FOUND)
|
|
22
22
|
*
|
|
23
|
-
* Domain-specific codes (CAMP_001,
|
|
23
|
+
* Domain-specific codes (e.g. CAMP_001, LAUNCH_001) are categorized by their
|
|
24
24
|
* suffix shape — *_NOT_FOUND maps to 6, others fall through to 1.
|
|
25
25
|
*/
|
|
26
26
|
export declare function exitCodeForMessage(message: string): number;
|
|
27
27
|
export declare function handleError(error: unknown): never;
|
|
28
28
|
/**
|
|
29
|
-
* Parse the `[CODE] message (field: x)` string format that
|
|
30
|
-
* builds, recovering the structured fields.
|
|
29
|
+
* Parse the `[CODE] message (field: x)` string format that the API client's
|
|
30
|
+
* handleError builds, recovering the structured fields.
|
|
31
31
|
*
|
|
32
32
|
* Returns `{code, message, field?}`. Falls back to `{code: "UNKNOWN_ERROR", message}`
|
|
33
33
|
* when no bracketed code is present (shouldn't happen for API errors, but safe
|
|
@@ -42,7 +42,7 @@ export declare function parseErrorMessage(raw: string): {
|
|
|
42
42
|
* Format an error caught in an MCP tool handler as the structured response
|
|
43
43
|
* the MCP SDK returns to the model.
|
|
44
44
|
*
|
|
45
|
-
* Returns the canonical
|
|
45
|
+
* Returns the canonical v1 error envelope `{success: false, error: {code, message, field?}}`
|
|
46
46
|
* as JSON text inside the MCP `content` block, plus `isError: true` so the SDK
|
|
47
47
|
* surfaces it as an error to the model. Lets the LLM read `error.code`
|
|
48
48
|
* structurally instead of regexing it out of a flattened message.
|
package/dist/cli-utils.js
CHANGED
|
@@ -26,7 +26,7 @@ export function formatOutput(data, format = "json") {
|
|
|
26
26
|
console.log(JSON.stringify(data, null, 2));
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Map
|
|
29
|
+
* Map a fleet error-code prefix to a CLI exit code.
|
|
30
30
|
*
|
|
31
31
|
* Convention so shell scripts can branch on failure type without parsing stderr:
|
|
32
32
|
* - 0: success (not handled here)
|
|
@@ -37,7 +37,7 @@ export function formatOutput(data, format = "json") {
|
|
|
37
37
|
* - 5: payment / billing (PAYMENT_*, BILLING_*)
|
|
38
38
|
* - 6: not found (any *_NOT_FOUND)
|
|
39
39
|
*
|
|
40
|
-
* Domain-specific codes (CAMP_001,
|
|
40
|
+
* Domain-specific codes (e.g. CAMP_001, LAUNCH_001) are categorized by their
|
|
41
41
|
* suffix shape — *_NOT_FOUND maps to 6, others fall through to 1.
|
|
42
42
|
*/
|
|
43
43
|
export function exitCodeForMessage(message) {
|
|
@@ -75,8 +75,8 @@ export function handleError(error) {
|
|
|
75
75
|
process.exit(exitCodeForMessage(message));
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* Parse the `[CODE] message (field: x)` string format that
|
|
79
|
-
* builds, recovering the structured fields.
|
|
78
|
+
* Parse the `[CODE] message (field: x)` string format that the API client's
|
|
79
|
+
* handleError builds, recovering the structured fields.
|
|
80
80
|
*
|
|
81
81
|
* Returns `{code, message, field?}`. Falls back to `{code: "UNKNOWN_ERROR", message}`
|
|
82
82
|
* when no bracketed code is present (shouldn't happen for API errors, but safe
|
|
@@ -97,7 +97,7 @@ export function parseErrorMessage(raw) {
|
|
|
97
97
|
* Format an error caught in an MCP tool handler as the structured response
|
|
98
98
|
* the MCP SDK returns to the model.
|
|
99
99
|
*
|
|
100
|
-
* Returns the canonical
|
|
100
|
+
* Returns the canonical v1 error envelope `{success: false, error: {code, message, field?}}`
|
|
101
101
|
* as JSON text inside the MCP `content` block, plus `isError: true` so the SDK
|
|
102
102
|
* surfaces it as an error to the model. Lets the LLM read `error.code`
|
|
103
103
|
* structurally instead of regexing it out of a flattened message.
|
package/dist/commands/ai.js
CHANGED
|
@@ -3,11 +3,12 @@ export function registerAICommands(program, client) {
|
|
|
3
3
|
const ai = program.command("ai").description("AI content generation and analysis");
|
|
4
4
|
ai.command("generate")
|
|
5
5
|
.description("generate AI content (email draft, subject line, etc.)")
|
|
6
|
-
.requiredOption("-t, --generation-type <type>", "generation type
|
|
6
|
+
.requiredOption("-t, --generation-type <type>", "generation type: initial_outreach, follow_up, response_negotiation, meeting_request, or closing")
|
|
7
7
|
.requiredOption("--tone <tone>", "tone (e.g. professional, casual, friendly)")
|
|
8
8
|
.option("--contact-id <id>", "contact ID for personalization")
|
|
9
9
|
.option("--campaign-id <id>", "campaign ID for context")
|
|
10
10
|
.option("--prompt <text>", "free-form prompt context")
|
|
11
|
+
.option("--format <format>", "email (default) or dm - dm is short platform-native text, empty subject")
|
|
11
12
|
.action(async (opts) => {
|
|
12
13
|
try {
|
|
13
14
|
const body = {
|
|
@@ -20,6 +21,8 @@ export function registerAICommands(program, client) {
|
|
|
20
21
|
body.campaign_id = opts.campaignId;
|
|
21
22
|
if (opts.prompt)
|
|
22
23
|
body.prompt = opts.prompt;
|
|
24
|
+
if (opts.format)
|
|
25
|
+
body.format = opts.format;
|
|
23
26
|
const result = await client.post("/ai/generate", body);
|
|
24
27
|
formatOutput(result, getFormat(program.opts()));
|
|
25
28
|
}
|
|
@@ -11,7 +11,7 @@ export function registerConversationCommands(program, client) {
|
|
|
11
11
|
.option("-s, --page-size <n>", "items per page (max 100)", "50")
|
|
12
12
|
.option("--status <status>", "filter by status")
|
|
13
13
|
.option("--campaign-id <id>", "filter by campaign")
|
|
14
|
-
.option("--channel <channel>", "filter by channel: email, instagram, twitter")
|
|
14
|
+
.option("--channel <channel>", "filter by channel: email, instagram, twitter, tiktok")
|
|
15
15
|
.action(async (opts) => {
|
|
16
16
|
try {
|
|
17
17
|
const params = {};
|
|
@@ -44,6 +44,44 @@ export function registerConversationCommands(program, client) {
|
|
|
44
44
|
handleError(error);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
|
+
conversations
|
|
48
|
+
.command("create")
|
|
49
|
+
.description("find-or-create the hub conversation for a contact (not tied to a campaign)")
|
|
50
|
+
.requiredOption("--contact-id <id>", "contact ID to converse with")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await client.post("/conversations", { contact_id: opts.contactId });
|
|
54
|
+
formatOutput(result, getFormat(program.opts()));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
handleError(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
conversations
|
|
61
|
+
.command("send <id>")
|
|
62
|
+
.description("create a message in a conversation (status pending_approval routes to the review queue)")
|
|
63
|
+
.requiredOption("--content <text>", "message content")
|
|
64
|
+
.requiredOption("--direction <direction>", "inbound or outbound")
|
|
65
|
+
.requiredOption("--status <status>", "draft, queued, sending, pending_approval, sent, delivered, failed, bounced, or rejected")
|
|
66
|
+
.requiredOption("--channel <channel>", "email, twitter, instagram, or tiktok (tiktok records only - manual send)")
|
|
67
|
+
.option("--platform-recipient-id <id>", "platform recipient id (twitter/instagram channels)")
|
|
68
|
+
.action(async (id, opts) => {
|
|
69
|
+
try {
|
|
70
|
+
const body = {
|
|
71
|
+
content: opts.content,
|
|
72
|
+
direction: opts.direction,
|
|
73
|
+
status: opts.status,
|
|
74
|
+
channel: opts.channel,
|
|
75
|
+
};
|
|
76
|
+
if (opts.platformRecipientId)
|
|
77
|
+
body.platform_recipient_id = opts.platformRecipientId;
|
|
78
|
+
const result = await client.post(`/conversations/${id}/messages`, body);
|
|
79
|
+
formatOutput(result, getFormat(program.opts()));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
handleError(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
47
85
|
conversations
|
|
48
86
|
.command("messages <id>")
|
|
49
87
|
.description("list messages in a conversation")
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,9 @@ import { registerNotificationTools } from "./tools/notifications.js";
|
|
|
16
16
|
import { registerKeyTools } from "./tools/keys.js";
|
|
17
17
|
import { registerWebhookTools } from "./tools/webhooks.js";
|
|
18
18
|
import { registerAgentCreditTools } from "./tools/agent-credits.js";
|
|
19
|
+
import { registerUsageTools } from "./tools/usage.js";
|
|
20
|
+
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
21
|
+
import { registerAudienceTools } from "./tools/audiences.js";
|
|
19
22
|
const requireFromHere = createRequire(import.meta.url);
|
|
20
23
|
const pkg = requireFromHere("../package.json");
|
|
21
24
|
const server = new McpServer({
|
|
@@ -49,5 +52,8 @@ registerNotificationTools(server, client);
|
|
|
49
52
|
registerKeyTools(server, client);
|
|
50
53
|
registerWebhookTools(server, client);
|
|
51
54
|
registerAgentCreditTools(server, client);
|
|
55
|
+
registerUsageTools(server, client);
|
|
56
|
+
registerAutopilotTools(server, client);
|
|
57
|
+
registerAudienceTools(server, client);
|
|
52
58
|
const transport = new StdioServerTransport();
|
|
53
59
|
await server.connect(transport);
|
package/dist/tools/ai.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatMcpError } from "../cli-utils.js";
|
|
3
3
|
export function registerAiTools(server, client) {
|
|
4
|
-
server.tool("generate_content", "generate AI content for a
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
server.tool("generate_content", "generate AI outreach content (email or DM draft) for a contact/campaign. the contact's tags + enrichment_data feed the generation context automatically. requires generate scope.", {
|
|
5
|
+
generation_type: z
|
|
6
|
+
.enum(["initial_outreach", "follow_up", "response_negotiation", "meeting_request", "closing"])
|
|
7
|
+
.describe("type of content to generate"),
|
|
8
|
+
tone: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe("tone (e.g. professional, friendly, casual, formal, persuasive, informative)"),
|
|
12
|
+
campaign_id: z.string().uuid().optional().describe("campaign to scope the generation to"),
|
|
13
|
+
contact_id: z
|
|
14
|
+
.string()
|
|
15
|
+
.uuid()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("target contact - its tags + enrichment_data are used automatically"),
|
|
18
|
+
prompt: z.string().optional().describe("additional freeform instructions"),
|
|
19
|
+
format: z
|
|
20
|
+
.enum(["email", "dm"])
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("dm = short platform-native message text with empty subject. defaults to email"),
|
|
23
|
+
include_cta: z.boolean().optional().describe("include a clear call-to-action"),
|
|
24
|
+
}, async ({ include_cta, ...rest }) => {
|
|
10
25
|
try {
|
|
11
|
-
const
|
|
26
|
+
const body = { ...rest };
|
|
27
|
+
if (include_cta !== undefined)
|
|
28
|
+
body.input_context = { include_cta };
|
|
29
|
+
const result = await client.post("/ai/generate", body);
|
|
12
30
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
13
31
|
}
|
|
14
32
|
catch (error) {
|
|
@@ -17,7 +35,10 @@ export function registerAiTools(server, client) {
|
|
|
17
35
|
});
|
|
18
36
|
server.tool("generate_campaign_content", "generate campaign-level outreach content (email sequence, templates) from a campaign brief. content-first: takes the strategy inline, not a campaign id. requires generate scope.", {
|
|
19
37
|
campaign_name: z.string().min(1).describe("campaign name (used as the brief title)"),
|
|
20
|
-
objective: z
|
|
38
|
+
objective: z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.describe("campaign objective (e.g. 'book DJ gigs', 'sell SaaS to VPs of Eng')"),
|
|
21
42
|
target_audience: z.string().min(1).describe("target audience description"),
|
|
22
43
|
key_benefits: z.array(z.string().min(1)).min(1).describe("non-empty list of key benefits to emphasise"),
|
|
23
44
|
tone: z.string().min(1).describe("tone (e.g. professional, casual, warm-direct)"),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatMcpError } from "../cli-utils.js";
|
|
3
|
+
const TARGET_PROFILE_TYPES = [
|
|
4
|
+
"local_business",
|
|
5
|
+
"content_creator",
|
|
6
|
+
"influencer",
|
|
7
|
+
"b2b_professional",
|
|
8
|
+
"ecommerce",
|
|
9
|
+
"saas_company",
|
|
10
|
+
"agency",
|
|
11
|
+
"freelancer",
|
|
12
|
+
"other",
|
|
13
|
+
];
|
|
14
|
+
export function registerAudienceTools(server, client) {
|
|
15
|
+
server.tool("list_audiences", "list the audience library - reusable target profiles created once and attachable to any campaign. requires read scope.", {
|
|
16
|
+
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
17
|
+
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
18
|
+
}, async ({ page, page_size }) => {
|
|
19
|
+
try {
|
|
20
|
+
const params = {};
|
|
21
|
+
if (page !== undefined)
|
|
22
|
+
params.page = String(page);
|
|
23
|
+
if (page_size !== undefined)
|
|
24
|
+
params.page_size = String(page_size);
|
|
25
|
+
const result = await client.get("/audiences", params);
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return formatMcpError(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("create_audience", "create a library audience profile manually. names are unique per library (case-insensitive). requires write scope.", {
|
|
33
|
+
name: z.string().min(1).max(100).describe("audience name"),
|
|
34
|
+
profile_type: z.enum(TARGET_PROFILE_TYPES).describe("target profile type"),
|
|
35
|
+
description: z.string().max(500).optional().describe("who this audience is"),
|
|
36
|
+
fit_score: z.number().int().min(0).max(100).optional().describe("how well this audience fits (0-100)"),
|
|
37
|
+
job_titles: z.array(z.string().max(100)).max(10).optional().describe("target job titles"),
|
|
38
|
+
industries: z.array(z.string().max(100)).max(10).optional().describe("target industries"),
|
|
39
|
+
pain_points: z.array(z.string().max(100)).max(10).optional().describe("problems they feel"),
|
|
40
|
+
buying_signals: z.array(z.string().max(100)).max(10).optional().describe("signals they're ready to buy"),
|
|
41
|
+
discovery_keywords: z
|
|
42
|
+
.array(z.string().max(100))
|
|
43
|
+
.max(10)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("keywords for contact discovery"),
|
|
46
|
+
outreach_angle: z.string().max(500).optional().describe("the hook messages should lead with"),
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
try {
|
|
49
|
+
const body = Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
50
|
+
const result = await client.post("/audiences", body);
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return formatMcpError(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.tool("generate_audiences", "AI-generate audience profiles from a product (scrape + analysis) into the library. generated names already in the library are skipped, not overwritten. rate limited to 5 per hour. requires generate scope.", {
|
|
58
|
+
product_name: z.string().min(1).max(200).describe("product name"),
|
|
59
|
+
product_url: z.string().url().optional().describe("product website - scraped for extra context"),
|
|
60
|
+
product_description: z.string().max(2000).optional().describe("what it does and who it's for"),
|
|
61
|
+
}, async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
const body = Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
64
|
+
const result = await client.post("/audiences/generate", body);
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return formatMcpError(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
server.tool("attach_audience_to_campaign", "copy a library audience into a campaign. the response is the campaign's own copy (a target profile) - editing it never touches the library master. if the campaign already has a same-named profile, the existing copy is returned. requires write scope.", {
|
|
72
|
+
id: z.string().uuid().describe("audience (library profile) ID"),
|
|
73
|
+
campaign_id: z
|
|
74
|
+
.string()
|
|
75
|
+
.uuid()
|
|
76
|
+
.describe("campaign to attach to (must be yours and not completed or deleted)"),
|
|
77
|
+
}, async ({ id, campaign_id }) => {
|
|
78
|
+
try {
|
|
79
|
+
const result = await client.post(`/audiences/${id}/attach`, {
|
|
80
|
+
campaign_id,
|
|
81
|
+
});
|
|
82
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return formatMcpError(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
server.tool("delete_audience", "delete a library audience profile. campaigns it was attached to keep their copy. requires write scope.", {
|
|
89
|
+
id: z.string().uuid().describe("audience (library profile) ID"),
|
|
90
|
+
}, async ({ id }) => {
|
|
91
|
+
try {
|
|
92
|
+
await client.delete(`/audiences/${id}`);
|
|
93
|
+
return { content: [{ type: "text", text: `audience ${id} deleted` }] };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return formatMcpError(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatMcpError } from "../cli-utils.js";
|
|
3
|
+
export function registerAutopilotTools(server, client) {
|
|
4
|
+
server.tool("get_autopilot_status", "get autopilot status: whether it's configured, the current config, the pending-review count for the standing campaign, and the latest run's step results. requires read scope.", {}, async () => {
|
|
5
|
+
try {
|
|
6
|
+
const result = await client.get("/autopilot");
|
|
7
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
return formatMcpError(error);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
server.tool("configure_autopilot", "create or update the autopilot config (merge: only the fields you send change; first call creates the config with defaults). autopilot is review-only - drafts wait in the approval queue, nothing sends without approval. requires write scope.", {
|
|
14
|
+
enabled: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("turn the nightly loop on or off (enabling requires a campaign_id and clears pause state)"),
|
|
18
|
+
campaign_id: z
|
|
19
|
+
.string()
|
|
20
|
+
.uuid()
|
|
21
|
+
.nullable()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("the ONE standing campaign autopilot tends (draft, scheduled, or active); null to unset"),
|
|
24
|
+
max_new_contacts_per_run: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(10)
|
|
28
|
+
.max(50)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("discovery batch size per nightly run (default 10)"),
|
|
31
|
+
max_pending_reviews: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.min(5)
|
|
35
|
+
.max(200)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("skip discovery while this many drafts sit unreviewed (default 25)"),
|
|
38
|
+
run_hour: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.min(0)
|
|
42
|
+
.max(23)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("local hour the nightly run fires (default 2)"),
|
|
45
|
+
timezone: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(64)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("IANA timezone for run_hour (default America/New_York)"),
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
try {
|
|
53
|
+
const body = Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
54
|
+
const result = await client.patch("/autopilot", body);
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return formatMcpError(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.tool("run_autopilot_now", "queue a manual autopilot run immediately (insights refresh + discovery top-up on the standing campaign). steps that already completed today are skipped. rate limited to 2 per hour. requires generate scope.", {}, async () => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.post("/autopilot/run");
|
|
64
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return formatMcpError(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatMcpError } from "../cli-utils.js";
|
|
3
3
|
const CONVERSATION_STATUSES = ["active", "completed", "failed", "human_review"];
|
|
4
|
+
const MESSAGE_STATUSES = [
|
|
5
|
+
"draft",
|
|
6
|
+
"queued",
|
|
7
|
+
"sending",
|
|
8
|
+
"pending_approval",
|
|
9
|
+
"sent",
|
|
10
|
+
"delivered",
|
|
11
|
+
"failed",
|
|
12
|
+
"bounced",
|
|
13
|
+
"rejected",
|
|
14
|
+
];
|
|
4
15
|
export function registerConversationTools(server, client) {
|
|
5
16
|
server.tool("list_conversations", "list conversations with optional filtering. requires read scope.", {
|
|
6
17
|
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
@@ -36,6 +47,39 @@ export function registerConversationTools(server, client) {
|
|
|
36
47
|
return formatMcpError(error);
|
|
37
48
|
}
|
|
38
49
|
});
|
|
50
|
+
server.tool("create_conversation", "find-or-create the hub conversation for a contact (not tied to a campaign) - the entry point for starting outreach outside a campaign flow. requires write scope.", {
|
|
51
|
+
contact_id: z.string().uuid().describe("contact ID to converse with"),
|
|
52
|
+
}, async ({ contact_id }) => {
|
|
53
|
+
try {
|
|
54
|
+
const result = await client.post("/conversations", { contact_id });
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return formatMcpError(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.tool("create_conversation_message", "create a message in a conversation. status pending_approval places it in the human review queue instead of sending. requires write scope.", {
|
|
62
|
+
id: z.string().uuid().describe("conversation ID"),
|
|
63
|
+
content: z.string().min(1).describe("message content"),
|
|
64
|
+
direction: z.enum(["inbound", "outbound"]).describe("message direction"),
|
|
65
|
+
status: z.enum(MESSAGE_STATUSES).describe("initial message status"),
|
|
66
|
+
channel: z.enum(["email", "twitter", "instagram", "tiktok"]).describe("delivery channel (tiktok is manual-send: record only)"),
|
|
67
|
+
platform_recipient_id: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("platform recipient id (twitter/instagram channels)"),
|
|
71
|
+
}, async ({ id, content, direction, status, channel, platform_recipient_id }) => {
|
|
72
|
+
try {
|
|
73
|
+
const body = { content, direction, status, channel };
|
|
74
|
+
if (platform_recipient_id !== undefined)
|
|
75
|
+
body.platform_recipient_id = platform_recipient_id;
|
|
76
|
+
const result = await client.post(`/conversations/${id}/messages`, body);
|
|
77
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return formatMcpError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
39
83
|
server.tool("get_conversation_messages", "get all messages in a conversation. requires read scope.", {
|
|
40
84
|
id: z.string().uuid().describe("conversation ID"),
|
|
41
85
|
}, async ({ id }) => {
|
package/dist/tools/keys.js
CHANGED
|
@@ -41,9 +41,9 @@ export function registerKeyTools(server, client) {
|
|
|
41
41
|
scopes: z
|
|
42
42
|
.array(z.enum(SCOPES_WITH_WILDCARD))
|
|
43
43
|
.min(1, "scopes must be a non-empty array; omit the field for all scopes")
|
|
44
|
-
.refine((s) => !s.includes("*") || s.length === 1,
|
|
44
|
+
.refine((s) => !s.includes("*") || s.length === 1, 'wildcard scope "*" must be the only element; mixing it with named scopes is rejected to prevent accidental privilege escalation')
|
|
45
45
|
.optional()
|
|
46
|
-
.describe(
|
|
46
|
+
.describe('permission scopes — any subset of [read, write, generate, send, admin], or ["*"] for all. omit the field for all scopes. empty array and mixed wildcard (e.g. ["read", "*"]) are rejected with 422.'),
|
|
47
47
|
expires_at: z.string().optional().describe("expiration date in ISO 8601 format (must be in the future)"),
|
|
48
48
|
}, async (args) => {
|
|
49
49
|
try {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { IrisClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Unified plan-usage tool: current plan, quota usage, and billing period in
|
|
5
|
+
* one call (the campaign envelope plus per-campaign contact/message limits).
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerUsageTools(server: McpServer, client: IrisClient): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { formatMcpError } from "../cli-utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Unified plan-usage tool: current plan, quota usage, and billing period in
|
|
4
|
+
* one call (the campaign envelope plus per-campaign contact/message limits).
|
|
5
|
+
*/
|
|
6
|
+
export function registerUsageTools(server, client) {
|
|
7
|
+
server.tool("get_usage", "get your current plan, quota usage (campaigns used/remaining this period), and billing period (requires read scope)", {}, async () => {
|
|
8
|
+
try {
|
|
9
|
+
const result = await client.get("/usage");
|
|
10
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return formatMcpError(error);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -249,3 +249,54 @@ export interface WebhookDelivery {
|
|
|
249
249
|
response_body: string | null;
|
|
250
250
|
created_at: string;
|
|
251
251
|
}
|
|
252
|
+
export interface AutopilotConfig {
|
|
253
|
+
enabled: boolean;
|
|
254
|
+
campaign_id: string | null;
|
|
255
|
+
max_new_contacts_per_run: number;
|
|
256
|
+
max_pending_reviews: number;
|
|
257
|
+
run_hour: number;
|
|
258
|
+
timezone: string;
|
|
259
|
+
paused: boolean;
|
|
260
|
+
pause_reason: string | null;
|
|
261
|
+
created_at: string;
|
|
262
|
+
updated_at: string;
|
|
263
|
+
}
|
|
264
|
+
export interface AutopilotStatus {
|
|
265
|
+
configured: boolean;
|
|
266
|
+
config: AutopilotConfig | null;
|
|
267
|
+
pending_reviews: number;
|
|
268
|
+
last_run: {
|
|
269
|
+
run_date: string;
|
|
270
|
+
steps: Array<{
|
|
271
|
+
step: string;
|
|
272
|
+
status: string;
|
|
273
|
+
}>;
|
|
274
|
+
} | null;
|
|
275
|
+
}
|
|
276
|
+
export interface AutopilotRunQueued {
|
|
277
|
+
queued: boolean;
|
|
278
|
+
run_date: string;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Library (audience) profile — a reusable target profile owned by the user,
|
|
282
|
+
* not scoped to a campaign. Attaching copies it into a campaign, and the
|
|
283
|
+
* attach response is a TargetProfile (the campaign's own copy).
|
|
284
|
+
*/
|
|
285
|
+
export interface AudienceProfile {
|
|
286
|
+
id: string;
|
|
287
|
+
name: string;
|
|
288
|
+
description: string | null;
|
|
289
|
+
profile_type: string;
|
|
290
|
+
fit_score: number | null;
|
|
291
|
+
job_titles: string[];
|
|
292
|
+
industries: string[];
|
|
293
|
+
company_sizes: string[];
|
|
294
|
+
pain_points: string[];
|
|
295
|
+
buying_signals: string[];
|
|
296
|
+
discovery_sources: string[];
|
|
297
|
+
discovery_keywords: string[];
|
|
298
|
+
outreach_angle: string | null;
|
|
299
|
+
contacts_discovered: number;
|
|
300
|
+
created_at: string;
|
|
301
|
+
updated_at: string;
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jclvsh/iris",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "MCP server for the Iris API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
18
19
|
"start": "node dist/index.js"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|