Skip to content

Commit 80fc6d5

Browse files
authored
Merge pull request #114 from Recoupable-com/sweetmantech/myc-3923-create_new_artist-migrate-tool-from-recoup-chat-to-recoup
feat: migrate create_new_artist MCP tool from Recoup-Chat to recoup-api
2 parents 01c4dca + 6f041b9 commit 80fc6d5

File tree

12 files changed

+802
-0
lines changed

12 files changed

+802
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
3+
const mockInsertAccount = vi.fn();
4+
const mockInsertAccountInfo = vi.fn();
5+
const mockSelectAccountWithSocials = vi.fn();
6+
const mockInsertAccountArtistId = vi.fn();
7+
const mockAddArtistToOrganization = vi.fn();
8+
9+
vi.mock("@/lib/supabase/accounts/insertAccount", () => ({
10+
insertAccount: (...args: unknown[]) => mockInsertAccount(...args),
11+
}));
12+
13+
vi.mock("@/lib/supabase/account_info/insertAccountInfo", () => ({
14+
insertAccountInfo: (...args: unknown[]) => mockInsertAccountInfo(...args),
15+
}));
16+
17+
vi.mock("@/lib/supabase/accounts/selectAccountWithSocials", () => ({
18+
selectAccountWithSocials: (...args: unknown[]) => mockSelectAccountWithSocials(...args),
19+
}));
20+
21+
vi.mock("@/lib/supabase/account_artist_ids/insertAccountArtistId", () => ({
22+
insertAccountArtistId: (...args: unknown[]) => mockInsertAccountArtistId(...args),
23+
}));
24+
25+
vi.mock("@/lib/supabase/artist_organization_ids/addArtistToOrganization", () => ({
26+
addArtistToOrganization: (...args: unknown[]) => mockAddArtistToOrganization(...args),
27+
}));
28+
29+
import { createArtistInDb } from "../createArtistInDb";
30+
31+
describe("createArtistInDb", () => {
32+
const mockAccount = {
33+
id: "artist-123",
34+
name: "Test Artist",
35+
created_at: "2026-01-15T00:00:00Z",
36+
updated_at: "2026-01-15T00:00:00Z",
37+
};
38+
39+
const mockAccountInfo = {
40+
id: "info-123",
41+
account_id: "artist-123",
42+
image: null,
43+
instruction: null,
44+
knowledges: null,
45+
label: null,
46+
organization: null,
47+
company_name: null,
48+
job_title: null,
49+
role_type: null,
50+
onboarding_status: null,
51+
onboarding_data: null,
52+
};
53+
54+
const mockFullAccount = {
55+
...mockAccount,
56+
account_socials: [],
57+
account_info: [mockAccountInfo],
58+
};
59+
60+
beforeEach(() => {
61+
vi.clearAllMocks();
62+
});
63+
64+
it("creates an artist account with all required steps", async () => {
65+
mockInsertAccount.mockResolvedValue(mockAccount);
66+
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
67+
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
68+
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
69+
70+
const result = await createArtistInDb("Test Artist", "owner-456");
71+
72+
expect(mockInsertAccount).toHaveBeenCalledWith({ name: "Test Artist" });
73+
expect(mockInsertAccountInfo).toHaveBeenCalledWith({ account_id: "artist-123" });
74+
expect(mockSelectAccountWithSocials).toHaveBeenCalledWith("artist-123");
75+
expect(mockInsertAccountArtistId).toHaveBeenCalledWith("owner-456", "artist-123");
76+
expect(result).toMatchObject({
77+
id: "artist-123",
78+
account_id: "artist-123",
79+
name: "Test Artist",
80+
});
81+
});
82+
83+
it("links artist to organization when organizationId is provided", async () => {
84+
mockInsertAccount.mockResolvedValue(mockAccount);
85+
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
86+
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
87+
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
88+
mockAddArtistToOrganization.mockResolvedValue("org-rel-123");
89+
90+
const result = await createArtistInDb("Test Artist", "owner-456", "org-789");
91+
92+
expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789");
93+
expect(result).not.toBeNull();
94+
});
95+
96+
it("returns null when account creation fails", async () => {
97+
mockInsertAccount.mockRejectedValue(new Error("Insert failed"));
98+
99+
const result = await createArtistInDb("Test Artist", "owner-456");
100+
101+
expect(result).toBeNull();
102+
expect(mockInsertAccountInfo).not.toHaveBeenCalled();
103+
});
104+
105+
it("returns null when account info creation fails", async () => {
106+
mockInsertAccount.mockResolvedValue(mockAccount);
107+
mockInsertAccountInfo.mockResolvedValue(null);
108+
109+
const result = await createArtistInDb("Test Artist", "owner-456");
110+
111+
expect(result).toBeNull();
112+
expect(mockSelectAccountWithSocials).not.toHaveBeenCalled();
113+
});
114+
115+
it("returns null when fetching full account data fails", async () => {
116+
mockInsertAccount.mockResolvedValue(mockAccount);
117+
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
118+
mockSelectAccountWithSocials.mockResolvedValue(null);
119+
120+
const result = await createArtistInDb("Test Artist", "owner-456");
121+
122+
expect(result).toBeNull();
123+
expect(mockInsertAccountArtistId).not.toHaveBeenCalled();
124+
});
125+
126+
it("returns null when associating artist with owner fails", async () => {
127+
mockInsertAccount.mockResolvedValue(mockAccount);
128+
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
129+
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
130+
mockInsertAccountArtistId.mockRejectedValue(new Error("Association failed"));
131+
132+
const result = await createArtistInDb("Test Artist", "owner-456");
133+
134+
expect(result).toBeNull();
135+
});
136+
});

lib/artists/createArtistInDb.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
2+
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
3+
import {
4+
selectAccountWithSocials,
5+
type AccountWithSocials,
6+
} from "@/lib/supabase/accounts/selectAccountWithSocials";
7+
import { insertAccountArtistId } from "@/lib/supabase/account_artist_ids/insertAccountArtistId";
8+
import { addArtistToOrganization } from "@/lib/supabase/artist_organization_ids/addArtistToOrganization";
9+
10+
/**
11+
* Result of creating an artist in the database.
12+
*/
13+
export type CreateArtistResult = AccountWithSocials & {
14+
account_id: string;
15+
};
16+
17+
/**
18+
* Create a new artist account in the database and associate it with an owner account.
19+
*
20+
* @param name - Name of the artist to create
21+
* @param accountId - ID of the owner account that will have access to this artist
22+
* @param organizationId - Optional organization ID to link the new artist to
23+
* @returns Created artist object or null if creation failed
24+
*/
25+
export async function createArtistInDb(
26+
name: string,
27+
accountId: string,
28+
organizationId?: string,
29+
): Promise<CreateArtistResult | null> {
30+
try {
31+
// Step 1: Create the account
32+
const account = await insertAccount({ name });
33+
34+
// Step 2: Create account info for the account
35+
const accountInfo = await insertAccountInfo({ account_id: account.id });
36+
if (!accountInfo) return null;
37+
38+
// Step 3: Get the full account data with socials and info
39+
const artist = await selectAccountWithSocials(account.id);
40+
if (!artist) return null;
41+
42+
// Step 4: Associate the artist with the owner via account_artist_ids
43+
await insertAccountArtistId(accountId, account.id);
44+
45+
// Step 5: Link to organization if provided
46+
if (organizationId) {
47+
await addArtistToOrganization(account.id, organizationId);
48+
}
49+
50+
return {
51+
...artist,
52+
account_id: artist.id,
53+
};
54+
} catch (error) {
55+
return null;
56+
}
57+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
4+
const mockCreateArtistInDb = vi.fn();
5+
const mockCopyRoom = vi.fn();
6+
7+
vi.mock("@/lib/artists/createArtistInDb", () => ({
8+
createArtistInDb: (...args: unknown[]) => mockCreateArtistInDb(...args),
9+
}));
10+
11+
vi.mock("@/lib/rooms/copyRoom", () => ({
12+
copyRoom: (...args: unknown[]) => mockCopyRoom(...args),
13+
}));
14+
15+
import { registerCreateNewArtistTool } from "../registerCreateNewArtistTool";
16+
17+
describe("registerCreateNewArtistTool", () => {
18+
let mockServer: McpServer;
19+
let registeredHandler: (args: unknown) => Promise<unknown>;
20+
21+
beforeEach(() => {
22+
vi.clearAllMocks();
23+
24+
mockServer = {
25+
registerTool: vi.fn((name, config, handler) => {
26+
registeredHandler = handler;
27+
}),
28+
} as unknown as McpServer;
29+
30+
registerCreateNewArtistTool(mockServer);
31+
});
32+
33+
it("registers the create_new_artist tool", () => {
34+
expect(mockServer.registerTool).toHaveBeenCalledWith(
35+
"create_new_artist",
36+
expect.objectContaining({
37+
description: expect.stringContaining("Create a new artist account"),
38+
}),
39+
expect.any(Function),
40+
);
41+
});
42+
43+
it("creates an artist and returns success", async () => {
44+
const mockArtist = {
45+
id: "artist-123",
46+
account_id: "artist-123",
47+
name: "Test Artist",
48+
account_info: [{ image: null }],
49+
account_socials: [],
50+
};
51+
mockCreateArtistInDb.mockResolvedValue(mockArtist);
52+
53+
const result = await registeredHandler({
54+
name: "Test Artist",
55+
account_id: "owner-456",
56+
});
57+
58+
expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", undefined);
59+
expect(result).toEqual({
60+
content: [
61+
{
62+
type: "text",
63+
text: expect.stringContaining("Successfully created artist"),
64+
},
65+
],
66+
});
67+
});
68+
69+
it("copies room when active_conversation_id is provided", async () => {
70+
const mockArtist = {
71+
id: "artist-123",
72+
account_id: "artist-123",
73+
name: "Test Artist",
74+
account_info: [{ image: null }],
75+
account_socials: [],
76+
};
77+
mockCreateArtistInDb.mockResolvedValue(mockArtist);
78+
mockCopyRoom.mockResolvedValue("new-room-789");
79+
80+
const result = await registeredHandler({
81+
name: "Test Artist",
82+
account_id: "owner-456",
83+
active_conversation_id: "source-room-111",
84+
});
85+
86+
expect(mockCopyRoom).toHaveBeenCalledWith("source-room-111", "artist-123");
87+
expect(result).toEqual({
88+
content: [
89+
{
90+
type: "text",
91+
text: expect.stringContaining("new-room-789"),
92+
},
93+
],
94+
});
95+
});
96+
97+
it("passes organization_id to createArtistInDb", async () => {
98+
const mockArtist = {
99+
id: "artist-123",
100+
account_id: "artist-123",
101+
name: "Test Artist",
102+
account_info: [{ image: null }],
103+
account_socials: [],
104+
};
105+
mockCreateArtistInDb.mockResolvedValue(mockArtist);
106+
107+
await registeredHandler({
108+
name: "Test Artist",
109+
account_id: "owner-456",
110+
organization_id: "org-999",
111+
});
112+
113+
expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", "org-999");
114+
});
115+
116+
it("returns error when artist creation fails", async () => {
117+
mockCreateArtistInDb.mockResolvedValue(null);
118+
119+
const result = await registeredHandler({
120+
name: "Test Artist",
121+
account_id: "owner-456",
122+
});
123+
124+
expect(result).toEqual({
125+
content: [
126+
{
127+
type: "text",
128+
text: expect.stringContaining("Failed to create artist"),
129+
},
130+
],
131+
});
132+
});
133+
134+
it("returns error with message when exception is thrown", async () => {
135+
mockCreateArtistInDb.mockRejectedValue(new Error("Database connection failed"));
136+
137+
const result = await registeredHandler({
138+
name: "Test Artist",
139+
account_id: "owner-456",
140+
});
141+
142+
expect(result).toEqual({
143+
content: [
144+
{
145+
type: "text",
146+
text: expect.stringContaining("Database connection failed"),
147+
},
148+
],
149+
});
150+
});
151+
});

lib/mcp/tools/artists/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { registerCreateNewArtistTool } from "./registerCreateNewArtistTool";
3+
4+
/**
5+
* Registers all artist-related MCP tools on the server.
6+
*
7+
* @param server - The MCP server instance to register tools on.
8+
*/
9+
export const registerAllArtistTools = (server: McpServer): void => {
10+
registerCreateNewArtistTool(server);
11+
};

0 commit comments

Comments
 (0)