Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,17 @@ describe("MissiveClient", () => {
expect(body.posts.organization).toBe("org-1");
});
});

describe("listOrganizations", () => {
it("calls GET /organizations", async () => {
const fetch = mockFetch({ organizations: [{ id: "org-1", name: "Test Org" }] });
vi.stubGlobal("fetch", fetch);
const result = await client.listOrganizations();
expect(fetch).toHaveBeenCalledWith(
"https://api.test.com/v1/organizations",
expect.objectContaining({ method: "GET" }),
);
expect(result.organizations).toEqual([{ id: "org-1", name: "Test Org" }]);
});
});
});
7 changes: 7 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ContactBook,
CreateContactParams,
UpdateContactParams,
Organization,
} from "./types.js";

export class MissiveApiError extends Error {
Expand Down Expand Up @@ -210,6 +211,12 @@ export class MissiveClient {
});
}

// --- Organizations ---

async listOrganizations(): Promise<{ organizations: Organization[] }> {
return this.request("GET", "/organizations");
}

// --- Contact Books ---

async listContactBooks(params?: {
Expand Down
24 changes: 20 additions & 4 deletions src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ function err(e: unknown): { content: Array<{ type: "text"; text: string }>; isEr
return { content: [{ type: "text" as const, text: String(e) }], isError: true };
}

// --- Organization resolution ---

let cachedOrgId: string | undefined;

async function resolveOrgId(explicit?: string): Promise<string> {
if (explicit) return explicit;
if (cachedOrgId) return cachedOrgId;
const { organizations } = await client.listOrganizations();
if (organizations.length === 0) throw new Error("No organizations found");
if (organizations.length > 1) throw new Error("Multiple organizations found — provide organization ID explicitly");
cachedOrgId = organizations[0].id;
return cachedOrgId;
}

// --- Server ---

const server = new McpServer({
Expand Down Expand Up @@ -121,13 +135,14 @@ server.registerTool("assign_conversation", {
inputSchema: {
id: z.string().describe("Conversation ID"),
users: z.array(z.string()).describe("User IDs to assign"),
organization: z.string().describe("Organization ID (required by Missive API)"),
organization: z.string().optional().describe("Organization ID (auto-resolved if omitted)"),
},
}, async ({ id, users, organization }) => {
try {
const orgId = await resolveOrgId(organization);
return ok(await client.performConversationAction(id, {
add_assignees: users,
organization,
organization: orgId,
}));
} catch (e) { return err(e); }
});
Expand All @@ -137,14 +152,15 @@ server.registerTool("label_conversation", {
description: "Add or remove shared labels on a conversation.",
inputSchema: {
id: z.string().describe("Conversation ID"),
organization: z.string().describe("Organization ID (required by Missive API)"),
organization: z.string().optional().describe("Organization ID (auto-resolved if omitted)"),
add: z.array(z.string()).optional().describe("Shared label IDs to add"),
remove: z.array(z.string()).optional().describe("Shared label IDs to remove"),
},
}, async ({ id, organization, add, remove }) => {
try {
const orgId = await resolveOrgId(organization);
return ok(await client.performConversationAction(id, {
organization,
organization: orgId,
add_shared_labels: add,
remove_shared_labels: remove,
}));
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ export interface UpdateContactParams {
infos?: ContactInfo[];
}

export interface Organization {
id: string;
name: string;
}

export interface ConversationActionParams {
close?: boolean;
add_to_inbox?: boolean;
Expand Down
Loading