-
Notifications
You must be signed in to change notification settings - Fork 6
feat: migrate create_new_artist MCP tool from Recoup-Chat to recoup-api #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sweetmantech
merged 5 commits into
test
from
sweetmantech/myc-3923-create_new_artist-migrate-tool-from-recoup-chat-to-recoup
Jan 15, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
839e2ab
feat: migrate create_new_artist MCP tool from Recoup-Chat to recoup-api
sweetmantech eefcb1a
fix: resolve type errors in createArtistInDb
sweetmantech e4be1cb
refactor: DRY CreateArtistResult type using AccountWithSocials
sweetmantech 976d559
feat: make account_id optional in create_new_artist MCP tool
sweetmantech 6f041b9
refactor: DRY CreateNewArtistResult using CreateArtistResult type
sweetmantech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
|
|
||
| const mockInsertAccount = vi.fn(); | ||
| const mockInsertAccountInfo = vi.fn(); | ||
| const mockSelectAccountWithSocials = vi.fn(); | ||
| const mockInsertAccountArtistId = vi.fn(); | ||
| const mockAddArtistToOrganization = vi.fn(); | ||
|
|
||
| vi.mock("@/lib/supabase/accounts/insertAccount", () => ({ | ||
| insertAccount: (...args: unknown[]) => mockInsertAccount(...args), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/supabase/account_info/insertAccountInfo", () => ({ | ||
| insertAccountInfo: (...args: unknown[]) => mockInsertAccountInfo(...args), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/supabase/accounts/selectAccountWithSocials", () => ({ | ||
| selectAccountWithSocials: (...args: unknown[]) => mockSelectAccountWithSocials(...args), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/supabase/account_artist_ids/insertAccountArtistId", () => ({ | ||
| insertAccountArtistId: (...args: unknown[]) => mockInsertAccountArtistId(...args), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/supabase/artist_organization_ids/addArtistToOrganization", () => ({ | ||
| addArtistToOrganization: (...args: unknown[]) => mockAddArtistToOrganization(...args), | ||
| })); | ||
|
|
||
| import { createArtistInDb } from "../createArtistInDb"; | ||
|
|
||
| describe("createArtistInDb", () => { | ||
| const mockAccount = { | ||
| id: "artist-123", | ||
| name: "Test Artist", | ||
| created_at: "2026-01-15T00:00:00Z", | ||
| updated_at: "2026-01-15T00:00:00Z", | ||
| }; | ||
|
|
||
| const mockAccountInfo = { | ||
| id: "info-123", | ||
| account_id: "artist-123", | ||
| image: null, | ||
| instruction: null, | ||
| knowledges: null, | ||
| label: null, | ||
| organization: null, | ||
| company_name: null, | ||
| job_title: null, | ||
| role_type: null, | ||
| onboarding_status: null, | ||
| onboarding_data: null, | ||
| }; | ||
|
|
||
| const mockFullAccount = { | ||
| ...mockAccount, | ||
| account_socials: [], | ||
| account_info: [mockAccountInfo], | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("creates an artist account with all required steps", async () => { | ||
| mockInsertAccount.mockResolvedValue(mockAccount); | ||
| mockInsertAccountInfo.mockResolvedValue(mockAccountInfo); | ||
| mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount); | ||
| mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" }); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456"); | ||
|
|
||
| expect(mockInsertAccount).toHaveBeenCalledWith({ name: "Test Artist" }); | ||
| expect(mockInsertAccountInfo).toHaveBeenCalledWith({ account_id: "artist-123" }); | ||
| expect(mockSelectAccountWithSocials).toHaveBeenCalledWith("artist-123"); | ||
| expect(mockInsertAccountArtistId).toHaveBeenCalledWith("owner-456", "artist-123"); | ||
| expect(result).toMatchObject({ | ||
| id: "artist-123", | ||
| account_id: "artist-123", | ||
| name: "Test Artist", | ||
| }); | ||
| }); | ||
|
|
||
| it("links artist to organization when organizationId is provided", async () => { | ||
| mockInsertAccount.mockResolvedValue(mockAccount); | ||
| mockInsertAccountInfo.mockResolvedValue(mockAccountInfo); | ||
| mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount); | ||
| mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" }); | ||
| mockAddArtistToOrganization.mockResolvedValue("org-rel-123"); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456", "org-789"); | ||
|
|
||
| expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789"); | ||
| expect(result).not.toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null when account creation fails", async () => { | ||
| mockInsertAccount.mockRejectedValue(new Error("Insert failed")); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456"); | ||
|
|
||
| expect(result).toBeNull(); | ||
| expect(mockInsertAccountInfo).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns null when account info creation fails", async () => { | ||
| mockInsertAccount.mockResolvedValue(mockAccount); | ||
| mockInsertAccountInfo.mockResolvedValue(null); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456"); | ||
|
|
||
| expect(result).toBeNull(); | ||
| expect(mockSelectAccountWithSocials).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns null when fetching full account data fails", async () => { | ||
| mockInsertAccount.mockResolvedValue(mockAccount); | ||
| mockInsertAccountInfo.mockResolvedValue(mockAccountInfo); | ||
| mockSelectAccountWithSocials.mockResolvedValue(null); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456"); | ||
|
|
||
| expect(result).toBeNull(); | ||
| expect(mockInsertAccountArtistId).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns null when associating artist with owner fails", async () => { | ||
| mockInsertAccount.mockResolvedValue(mockAccount); | ||
| mockInsertAccountInfo.mockResolvedValue(mockAccountInfo); | ||
| mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount); | ||
| mockInsertAccountArtistId.mockRejectedValue(new Error("Association failed")); | ||
|
|
||
| const result = await createArtistInDb("Test Artist", "owner-456"); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { insertAccount } from "@/lib/supabase/accounts/insertAccount"; | ||
| import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo"; | ||
| import { | ||
| selectAccountWithSocials, | ||
| type AccountWithSocials, | ||
| } from "@/lib/supabase/accounts/selectAccountWithSocials"; | ||
| import { insertAccountArtistId } from "@/lib/supabase/account_artist_ids/insertAccountArtistId"; | ||
| import { addArtistToOrganization } from "@/lib/supabase/artist_organization_ids/addArtistToOrganization"; | ||
|
|
||
| /** | ||
| * Result of creating an artist in the database. | ||
| */ | ||
| export type CreateArtistResult = AccountWithSocials & { | ||
| account_id: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Create a new artist account in the database and associate it with an owner account. | ||
| * | ||
| * @param name - Name of the artist to create | ||
| * @param accountId - ID of the owner account that will have access to this artist | ||
| * @param organizationId - Optional organization ID to link the new artist to | ||
| * @returns Created artist object or null if creation failed | ||
| */ | ||
| export async function createArtistInDb( | ||
| name: string, | ||
| accountId: string, | ||
| organizationId?: string, | ||
| ): Promise<CreateArtistResult | null> { | ||
| try { | ||
| // Step 1: Create the account | ||
| const account = await insertAccount({ name }); | ||
|
|
||
| // Step 2: Create account info for the account | ||
| const accountInfo = await insertAccountInfo({ account_id: account.id }); | ||
| if (!accountInfo) return null; | ||
|
|
||
| // Step 3: Get the full account data with socials and info | ||
| const artist = await selectAccountWithSocials(account.id); | ||
| if (!artist) return null; | ||
|
|
||
| // Step 4: Associate the artist with the owner via account_artist_ids | ||
| await insertAccountArtistId(accountId, account.id); | ||
|
|
||
| // Step 5: Link to organization if provided | ||
| if (organizationId) { | ||
| await addArtistToOrganization(account.id, organizationId); | ||
| } | ||
|
|
||
| return { | ||
| ...artist, | ||
| account_id: artist.id, | ||
| }; | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| } | ||
151 changes: 151 additions & 0 deletions
151
lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
|
|
||
| const mockCreateArtistInDb = vi.fn(); | ||
| const mockCopyRoom = vi.fn(); | ||
|
|
||
| vi.mock("@/lib/artists/createArtistInDb", () => ({ | ||
| createArtistInDb: (...args: unknown[]) => mockCreateArtistInDb(...args), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/rooms/copyRoom", () => ({ | ||
| copyRoom: (...args: unknown[]) => mockCopyRoom(...args), | ||
| })); | ||
|
|
||
| import { registerCreateNewArtistTool } from "../registerCreateNewArtistTool"; | ||
|
|
||
| describe("registerCreateNewArtistTool", () => { | ||
| let mockServer: McpServer; | ||
| let registeredHandler: (args: unknown) => Promise<unknown>; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
|
|
||
| mockServer = { | ||
| registerTool: vi.fn((name, config, handler) => { | ||
| registeredHandler = handler; | ||
| }), | ||
| } as unknown as McpServer; | ||
|
|
||
| registerCreateNewArtistTool(mockServer); | ||
| }); | ||
|
|
||
| it("registers the create_new_artist tool", () => { | ||
| expect(mockServer.registerTool).toHaveBeenCalledWith( | ||
| "create_new_artist", | ||
| expect.objectContaining({ | ||
| description: expect.stringContaining("Create a new artist account"), | ||
| }), | ||
| expect.any(Function), | ||
| ); | ||
| }); | ||
|
|
||
| it("creates an artist and returns success", async () => { | ||
| const mockArtist = { | ||
| id: "artist-123", | ||
| account_id: "artist-123", | ||
| name: "Test Artist", | ||
| account_info: [{ image: null }], | ||
| account_socials: [], | ||
| }; | ||
| mockCreateArtistInDb.mockResolvedValue(mockArtist); | ||
|
|
||
| const result = await registeredHandler({ | ||
| name: "Test Artist", | ||
| account_id: "owner-456", | ||
| }); | ||
|
|
||
| expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", undefined); | ||
| expect(result).toEqual({ | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: expect.stringContaining("Successfully created artist"), | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it("copies room when active_conversation_id is provided", async () => { | ||
| const mockArtist = { | ||
| id: "artist-123", | ||
| account_id: "artist-123", | ||
| name: "Test Artist", | ||
| account_info: [{ image: null }], | ||
| account_socials: [], | ||
| }; | ||
| mockCreateArtistInDb.mockResolvedValue(mockArtist); | ||
| mockCopyRoom.mockResolvedValue("new-room-789"); | ||
|
|
||
| const result = await registeredHandler({ | ||
| name: "Test Artist", | ||
| account_id: "owner-456", | ||
| active_conversation_id: "source-room-111", | ||
| }); | ||
|
|
||
| expect(mockCopyRoom).toHaveBeenCalledWith("source-room-111", "artist-123"); | ||
| expect(result).toEqual({ | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: expect.stringContaining("new-room-789"), | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it("passes organization_id to createArtistInDb", async () => { | ||
| const mockArtist = { | ||
| id: "artist-123", | ||
| account_id: "artist-123", | ||
| name: "Test Artist", | ||
| account_info: [{ image: null }], | ||
| account_socials: [], | ||
| }; | ||
| mockCreateArtistInDb.mockResolvedValue(mockArtist); | ||
|
|
||
| await registeredHandler({ | ||
| name: "Test Artist", | ||
| account_id: "owner-456", | ||
| organization_id: "org-999", | ||
| }); | ||
|
|
||
| expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", "org-999"); | ||
| }); | ||
|
|
||
| it("returns error when artist creation fails", async () => { | ||
| mockCreateArtistInDb.mockResolvedValue(null); | ||
|
|
||
| const result = await registeredHandler({ | ||
| name: "Test Artist", | ||
| account_id: "owner-456", | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: expect.stringContaining("Failed to create artist"), | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it("returns error with message when exception is thrown", async () => { | ||
| mockCreateArtistInDb.mockRejectedValue(new Error("Database connection failed")); | ||
|
|
||
| const result = await registeredHandler({ | ||
| name: "Test Artist", | ||
| account_id: "owner-456", | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: expect.stringContaining("Database connection failed"), | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { registerCreateNewArtistTool } from "./registerCreateNewArtistTool"; | ||
|
|
||
| /** | ||
| * Registers all artist-related MCP tools on the server. | ||
| * | ||
| * @param server - The MCP server instance to register tools on. | ||
| */ | ||
| export const registerAllArtistTools = (server: McpServer): void => { | ||
| registerCreateNewArtistTool(server); | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.