feat(integration): add spotify (#2347)

* Add spotify

* Finish spotify integration

* Fix logo

* fix build

* Rename tools

* Fix docs

* Fix lint

* Fix imports

* ran lint

---------

Co-authored-by: waleed <walif6@gmail.com>
This commit is contained in:
Siddharth Ganesan
2025-12-12 19:22:17 -08:00
committed by GitHub
parent 132f4bca38
commit ecf5209e6f
95 changed files with 10488 additions and 125 deletions

View File

@@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 496 512' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#1ed760'
d='M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z'
/>
<path d='M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z' />
</svg>
)
}

View File

@@ -89,6 +89,7 @@ import {
ShopifyIcon,
SlackIcon,
SmtpIcon,
SpotifyIcon,
SQSIcon,
SshIcon,
STTIcon,
@@ -118,115 +119,116 @@ import {
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
zoom: ZoomIcon,
zep: ZepIcon,
calendly: CalendlyIcon,
mailchimp: MailchimpIcon,
postgresql: PostgresIcon,
twilio_voice: TwilioIcon,
elasticsearch: ElasticsearchIcon,
rds: RDSIcon,
translate: TranslateIcon,
dynamodb: DynamoDBIcon,
wordpress: WordpressIcon,
tavily: TavilyIcon,
zendesk: ZendeskIcon,
youtube: YouTubeIcon,
x: xIcon,
wordpress: WordpressIcon,
wikipedia: WikipediaIcon,
whatsapp: WhatsAppIcon,
webflow: WebflowIcon,
wealthbox: WealthboxIcon,
vision: EyeIcon,
video_generator: VideoIcon,
typeform: TypeformIcon,
twilio_voice: TwilioIcon,
twilio_sms: TwilioIcon,
tts: TTSIcon,
trello: TrelloIcon,
translate: TranslateIcon,
thinking: BrainIcon,
telegram: TelegramIcon,
tavily: TavilyIcon,
supabase: SupabaseIcon,
stt: STTIcon,
stripe: StripeIcon,
stagehand: StagehandIcon,
ssh: SshIcon,
sqs: SQSIcon,
smtp: SmtpIcon,
slack: SlackIcon,
shopify: ShopifyIcon,
sharepoint: MicrosoftSharepointIcon,
sftp: SftpIcon,
serper: SerperIcon,
sentry: SentryIcon,
sendgrid: SendgridIcon,
search: SearchIcon,
salesforce: SalesforceIcon,
s3: S3Icon,
resend: ResendIcon,
reddit: RedditIcon,
rds: RDSIcon,
qdrant: QdrantIcon,
posthog: PosthogIcon,
postgresql: PostgresIcon,
polymarket: PolymarketIcon,
pipedrive: PipedriveIcon,
pinecone: PineconeIcon,
perplexity: PerplexityIcon,
parallel_ai: ParallelIcon,
outlook: OutlookIcon,
openai: OpenAIIcon,
onedrive: MicrosoftOneDriveIcon,
notion: NotionIcon,
neo4j: Neo4jIcon,
mysql: MySQLIcon,
mongodb: MongoDBIcon,
mistral_parse: MistralIcon,
microsoft_teams: MicrosoftTeamsIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_excel: MicrosoftExcelIcon,
memory: BrainIcon,
mem0: Mem0Icon,
mailgun: MailgunIcon,
mailchimp: MailchimpIcon,
linkup: LinkupIcon,
linkedin: LinkedInIcon,
linear: LinearIcon,
knowledge: PackageSearchIcon,
kalshi: KalshiIcon,
jira: JiraIcon,
jina: JinaAIIcon,
intercom: IntercomIcon,
incidentio: IncidentioIcon,
image_generator: ImageIcon,
hunter: HunterIOIcon,
huggingface: HuggingFaceIcon,
hubspot: HubspotIcon,
grafana: GrafanaIcon,
google_vault: GoogleVaultIcon,
google_slides: GoogleSlidesIcon,
google_sheets: GoogleSheetsIcon,
google_groups: GoogleGroupsIcon,
google_forms: GoogleFormsIcon,
google_drive: GoogleDriveIcon,
google_docs: GoogleDocsIcon,
google_calendar: GoogleCalendarIcon,
google_search: GoogleIcon,
gmail: GmailIcon,
gitlab: GitLabIcon,
github: GithubIcon,
firecrawl: FirecrawlIcon,
file: DocumentIcon,
exa: ExaAIIcon,
elevenlabs: ElevenLabsIcon,
elasticsearch: ElasticsearchIcon,
dynamodb: DynamoDBIcon,
duckduckgo: DuckDuckGoIcon,
dropbox: DropboxIcon,
discord: DiscordIcon,
datadog: DatadogIcon,
cursor: CursorIcon,
vision: EyeIcon,
zoom: ZoomIcon,
confluence: ConfluenceIcon,
clay: ClayIcon,
calendly: CalendlyIcon,
browser_use: BrowserUseIcon,
asana: AsanaIcon,
arxiv: ArxivIcon,
webflow: WebflowIcon,
pinecone: PineconeIcon,
apollo: ApolloIcon,
whatsapp: WhatsAppIcon,
typeform: TypeformIcon,
qdrant: QdrantIcon,
shopify: ShopifyIcon,
asana: AsanaIcon,
sqs: SQSIcon,
apify: ApifyIcon,
memory: BrainIcon,
gitlab: GitLabIcon,
polymarket: PolymarketIcon,
serper: SerperIcon,
linear: LinearIcon,
exa: ExaAIIcon,
telegram: TelegramIcon,
salesforce: SalesforceIcon,
hubspot: HubspotIcon,
hunter: HunterIOIcon,
linkup: LinkupIcon,
mongodb: MongoDBIcon,
airtable: AirtableIcon,
discord: DiscordIcon,
ahrefs: AhrefsIcon,
neo4j: Neo4jIcon,
tts: TTSIcon,
jina: JinaAIIcon,
google_docs: GoogleDocsIcon,
perplexity: PerplexityIcon,
google_search: GoogleIcon,
x: xIcon,
kalshi: KalshiIcon,
google_calendar: GoogleCalendarIcon,
zep: ZepIcon,
posthog: PosthogIcon,
grafana: GrafanaIcon,
google_slides: GoogleSlidesIcon,
microsoft_planner: MicrosoftPlannerIcon,
thinking: BrainIcon,
pipedrive: PipedriveIcon,
dropbox: DropboxIcon,
stagehand: StagehandIcon,
google_forms: GoogleFormsIcon,
file: DocumentIcon,
mistral_parse: MistralIcon,
gmail: GmailIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
incidentio: IncidentioIcon,
onedrive: MicrosoftOneDriveIcon,
resend: ResendIcon,
google_vault: GoogleVaultIcon,
sharepoint: MicrosoftSharepointIcon,
huggingface: HuggingFaceIcon,
sendgrid: SendgridIcon,
video_generator: VideoIcon,
smtp: SmtpIcon,
google_groups: GoogleGroupsIcon,
mailgun: MailgunIcon,
clay: ClayIcon,
jira: JiraIcon,
search: SearchIcon,
linkedin: LinkedInIcon,
wealthbox: WealthboxIcon,
notion: NotionIcon,
elevenlabs: ElevenLabsIcon,
microsoft_teams: MicrosoftTeamsIcon,
github: GithubIcon,
sftp: SftpIcon,
ssh: SshIcon,
google_drive: GoogleDriveIcon,
sentry: SentryIcon,
reddit: RedditIcon,
parallel_ai: ParallelIcon,
spotify: SpotifyIcon,
stripe: StripeIcon,
s3: S3Icon,
trello: TrelloIcon,
mem0: Mem0Icon,
knowledge: PackageSearchIcon,
intercom: IntercomIcon,
twilio_sms: TwilioIcon,
duckduckgo: DuckDuckGoIcon,
slack: SlackIcon,
datadog: DatadogIcon,
microsoft_excel: MicrosoftExcelIcon,
image_generator: ImageIcon,
google_sheets: GoogleSheetsIcon,
wikipedia: WikipediaIcon,
cursor: CursorIcon,
firecrawl: FirecrawlIcon,
mysql: MySQLIcon,
browser_use: BrowserUseIcon,
stt: STTIcon,
}

View File

@@ -324,7 +324,7 @@ Create an annotation on a dashboard or as a global annotation
| `organizationId` | string | No | Organization ID for multi-org Grafana instances |
| `text` | string | Yes | The text content of the annotation |
| `tags` | string | No | Comma-separated list of tags |
| `dashboardUid` | string | No | UID of the dashboard to add the annotation to \(optional for global annotations\) |
| `dashboardUid` | string | Yes | UID of the dashboard to add the annotation to |
| `panelId` | number | No | ID of the panel to add the annotation to |
| `time` | number | No | Start time in epoch milliseconds \(defaults to now\) |
| `timeEnd` | number | No | End time in epoch milliseconds \(for range annotations\) |
@@ -349,7 +349,7 @@ Query annotations by time range, dashboard, or tags
| `organizationId` | string | No | Organization ID for multi-org Grafana instances |
| `from` | number | No | Start time in epoch milliseconds |
| `to` | number | No | End time in epoch milliseconds |
| `dashboardUid` | string | No | Filter by dashboard UID |
| `dashboardUid` | string | Yes | Dashboard UID to query annotations from |
| `panelId` | number | No | Filter by panel ID |
| `tags` | string | No | Comma-separated list of tags to filter by |
| `type` | string | No | Filter by type \(alert or annotation\) |
@@ -490,6 +490,16 @@ Create a new folder in Grafana
| `uid` | string | The UID of the created folder |
| `title` | string | The title of the created folder |
| `url` | string | The URL path to the folder |
| `hasAcl` | boolean | Whether the folder has custom ACL permissions |
| `canSave` | boolean | Whether the current user can save the folder |
| `canEdit` | boolean | Whether the current user can edit the folder |
| `canAdmin` | boolean | Whether the current user has admin rights on the folder |
| `canDelete` | boolean | Whether the current user can delete the folder |
| `createdBy` | string | Username of who created the folder |
| `created` | string | Timestamp when the folder was created |
| `updatedBy` | string | Username of who last updated the folder |
| `updated` | string | Timestamp when the folder was last updated |
| `version` | number | Version number of the folder |

View File

@@ -85,6 +85,7 @@
"shopify",
"slack",
"smtp",
"spotify",
"sqs",
"ssh",
"stagehand",

File diff suppressed because it is too large Load Diff

View File

@@ -96,10 +96,7 @@ Retrieve user context from a thread with summary or basic mode
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `context` | string | The context string \(summary or basic\) |
| `facts` | array | Extracted facts |
| `entities` | array | Extracted entities |
| `summary` | string | Conversation summary |
| `context` | string | The context string \(summary or basic mode\) |
### `zep_get_messages`
@@ -139,9 +136,9 @@ Add messages to an existing thread
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `context` | string | Updated context after adding messages |
| `messageIds` | array | Array of added message UUIDs |
| `threadId` | string | The thread ID |
| `added` | boolean | Whether messages were added successfully |
| `messageIds` | array | Array of added message UUIDs |
### `zep_add_user`
@@ -211,7 +208,7 @@ List all conversation threads for a specific user
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `threads` | array | Array of thread objects for this user |
| `userId` | string | The user ID |
| `totalCount` | number | Total number of threads returned |

File diff suppressed because it is too large Load Diff

View File

@@ -101,6 +101,7 @@ import { SharepointBlock } from '@/blocks/blocks/sharepoint'
import { ShopifyBlock } from '@/blocks/blocks/shopify'
import { SlackBlock } from '@/blocks/blocks/slack'
import { SmtpBlock } from '@/blocks/blocks/smtp'
import { SpotifyBlock } from '@/blocks/blocks/spotify'
import { SSHBlock } from '@/blocks/blocks/ssh'
import { StagehandBlock } from '@/blocks/blocks/stagehand'
import { StartTriggerBlock } from '@/blocks/blocks/start_trigger'
@@ -241,6 +242,7 @@ export const registry: Record<string, BlockConfig> = {
sharepoint: SharepointBlock,
shopify: ShopifyBlock,
slack: SlackBlock,
spotify: SpotifyBlock,
smtp: SmtpBlock,
sftp: SftpBlock,
ssh: SSHBlock,

View File

@@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 496 512' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#1ed760'
d='M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z'
/>
<path d='M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z' />
</svg>
)
}

View File

@@ -222,6 +222,7 @@ export const auth = betterAuth({
'pipedrive',
'hubspot',
'linkedin',
'spotify',
// Common SSO provider patterns
...SSO_TRUSTED_PROVIDERS,
@@ -1838,6 +1839,72 @@ export const auth = betterAuth({
},
},
// Spotify provider
{
providerId: 'spotify',
clientId: env.SPOTIFY_CLIENT_ID as string,
clientSecret: env.SPOTIFY_CLIENT_SECRET as string,
authorizationUrl: 'https://accounts.spotify.com/authorize',
tokenUrl: 'https://accounts.spotify.com/api/token',
userInfoUrl: 'https://api.spotify.com/v1/me',
scopes: [
'user-read-private',
'user-read-email',
'user-library-read',
'user-library-modify',
'playlist-read-private',
'playlist-read-collaborative',
'playlist-modify-public',
'playlist-modify-private',
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
'user-read-recently-played',
'user-top-read',
'user-follow-read',
'user-follow-modify',
'user-read-playback-position',
'ugc-image-upload',
],
responseType: 'code',
authentication: 'basic',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/spotify`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Spotify user profile')
const response = await fetch('https://api.spotify.com/v1/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Failed to fetch Spotify user info', {
status: response.status,
statusText: response.statusText,
})
throw new Error('Failed to fetch user info')
}
const profile = await response.json()
return {
id: profile.id,
name: profile.display_name || 'Spotify User',
email: profile.email || `${profile.id}@spotify.user`,
emailVerified: true,
image: profile.images?.[0]?.url || undefined,
createdAt: new Date(),
updatedAt: new Date(),
}
} catch (error) {
logger.error('Error in Spotify getUserInfo:', { error })
return null
}
},
},
// WordPress.com provider
{
providerId: 'wordpress',

View File

@@ -230,6 +230,8 @@ export const env = createEnv({
ZOOM_CLIENT_SECRET: z.string().optional(), // Zoom OAuth client secret
WORDPRESS_CLIENT_ID: z.string().optional(), // WordPress.com OAuth client ID
WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

View File

@@ -175,9 +175,16 @@ export class LoggingSession {
logger.debug(`[${this.requestId}] Completed logging for execution ${this.executionId}`)
}
} catch (error) {
if (this.requestId) {
logger.error(`[${this.requestId}] Failed to complete logging:`, error)
}
// Always log completion failures with full details - these should not be silent
logger.error(`Failed to complete logging for execution ${this.executionId}:`, {
requestId: this.requestId,
workflowId: this.workflowId,
executionId: this.executionId,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
// Rethrow so safeComplete can decide what to do
throw error
}
}
@@ -247,12 +254,21 @@ export class LoggingSession {
}
if (this.requestId) {
logger.debug(`[${this.requestId}] Completed logging for execution ${this.executionId}`)
logger.debug(
`[${this.requestId}] Completed error logging for execution ${this.executionId}`
)
}
} catch (enhancedError) {
if (this.requestId) {
logger.error(`[${this.requestId}] Failed to complete logging:`, enhancedError)
}
// Always log completion failures with full details
logger.error(`Failed to complete error logging for execution ${this.executionId}:`, {
requestId: this.requestId,
workflowId: this.workflowId,
executionId: this.executionId,
error: enhancedError instanceof Error ? enhancedError.message : String(enhancedError),
stack: enhancedError instanceof Error ? enhancedError.stack : undefined,
})
// Rethrow so safeCompleteWithError can decide what to do
throw enhancedError
}
}
@@ -315,9 +331,10 @@ export class LoggingSession {
try {
await this.complete(params)
} catch (error) {
if (this.requestId) {
logger.error(`[${this.requestId}] Logging completion failed:`, error)
}
// Error already logged in complete(), log a summary here
logger.warn(
`[${this.requestId || 'unknown'}] Logging completion failed for execution ${this.executionId} - execution data not persisted`
)
}
}
@@ -325,9 +342,10 @@ export class LoggingSession {
try {
await this.completeWithError(error)
} catch (enhancedError) {
if (this.requestId) {
logger.error(`[${this.requestId}] Logging error completion failed:`, enhancedError)
}
// Error already logged in completeWithError(), log a summary here
logger.warn(
`[${this.requestId || 'unknown'}] Error logging completion failed for execution ${this.executionId} - execution data not persisted`
)
}
}
}

View File

@@ -31,6 +31,7 @@ import {
SalesforceIcon,
ShopifyIcon,
SlackIcon,
SpotifyIcon,
// SupabaseIcon,
TrelloIcon,
WealthboxIcon,
@@ -70,6 +71,7 @@ export type OAuthProvider =
| 'shopify'
| 'zoom'
| 'wordpress'
| 'spotify'
| string
export type OAuthService =
@@ -111,6 +113,8 @@ export type OAuthService =
| 'shopify'
| 'zoom'
| 'wordpress'
| 'spotify'
export interface OAuthProviderConfig {
id: OAuthProvider
name: string
@@ -891,6 +895,41 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'wordpress',
},
spotify: {
id: 'spotify',
name: 'Spotify',
icon: (props) => SpotifyIcon(props),
services: {
spotify: {
id: 'spotify',
name: 'Spotify',
description: 'Search music, manage playlists, control playback, and access your library.',
providerId: 'spotify',
icon: (props) => SpotifyIcon(props),
baseProviderIcon: (props) => SpotifyIcon(props),
scopes: [
'user-read-private',
'user-read-email',
'user-library-read',
'user-library-modify',
'playlist-read-private',
'playlist-read-collaborative',
'playlist-modify-public',
'playlist-modify-private',
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
'user-read-recently-played',
'user-top-read',
'user-follow-read',
'user-follow-modify',
'user-read-playback-position',
'ugc-image-upload',
],
},
},
defaultService: 'spotify',
},
}
/**
@@ -1470,6 +1509,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
supportsRefreshTokenRotation: false,
}
}
case 'spotify': {
const { clientId, clientSecret } = getCredentials(
env.SPOTIFY_CLIENT_ID,
env.SPOTIFY_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://accounts.spotify.com/api/token',
clientId,
clientSecret,
useBasicAuth: true,
supportsRefreshTokenRotation: false,
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}

View File

@@ -20,7 +20,7 @@
"test:coverage": "vitest run --coverage",
"email:dev": "email dev --dir components/emails",
"type-check": "tsc --noEmit",
"test:billing:suite": "bun run scripts/test-billing-suite.ts"
"generate-docs": "bun run ../../scripts/generate-docs.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",

View File

@@ -1014,6 +1014,86 @@ import {
} from '@/tools/slack'
import { smsSendTool } from '@/tools/sms'
import { smtpSendMailTool } from '@/tools/smtp'
import {
spotifyAddPlaylistCoverTool,
spotifyAddToQueueTool,
spotifyAddTracksToPlaylistTool,
spotifyCheckFollowingTool,
spotifyCheckPlaylistFollowersTool,
spotifyCheckSavedAlbumsTool,
spotifyCheckSavedAudiobooksTool,
spotifyCheckSavedEpisodesTool,
spotifyCheckSavedShowsTool,
spotifyCheckSavedTracksTool,
spotifyCreatePlaylistTool,
spotifyFollowArtistsTool,
spotifyFollowPlaylistTool,
spotifyGetAlbumsTool,
spotifyGetAlbumTool,
spotifyGetAlbumTracksTool,
spotifyGetArtistAlbumsTool,
spotifyGetArtistsTool,
spotifyGetArtistTool,
spotifyGetArtistTopTracksTool,
spotifyGetAudiobookChaptersTool,
spotifyGetAudiobooksTool,
spotifyGetAudiobookTool,
spotifyGetCategoriesTool,
spotifyGetCurrentlyPlayingTool,
spotifyGetCurrentUserTool,
spotifyGetDevicesTool,
spotifyGetEpisodesTool,
spotifyGetEpisodeTool,
spotifyGetFollowedArtistsTool,
spotifyGetMarketsTool,
spotifyGetNewReleasesTool,
spotifyGetPlaybackStateTool,
spotifyGetPlaylistCoverTool,
spotifyGetPlaylistTool,
spotifyGetPlaylistTracksTool,
spotifyGetQueueTool,
spotifyGetRecentlyPlayedTool,
spotifyGetSavedAlbumsTool,
spotifyGetSavedAudiobooksTool,
spotifyGetSavedEpisodesTool,
spotifyGetSavedShowsTool,
spotifyGetSavedTracksTool,
spotifyGetShowEpisodesTool,
spotifyGetShowsTool,
spotifyGetShowTool,
spotifyGetTopArtistsTool,
spotifyGetTopTracksTool,
spotifyGetTracksTool,
spotifyGetTrackTool,
spotifyGetUserPlaylistsTool,
spotifyGetUserProfileTool,
spotifyPauseTool,
spotifyPlayTool,
spotifyRemoveSavedAlbumsTool,
spotifyRemoveSavedAudiobooksTool,
spotifyRemoveSavedEpisodesTool,
spotifyRemoveSavedShowsTool,
spotifyRemoveSavedTracksTool,
spotifyRemoveTracksFromPlaylistTool,
spotifyReorderPlaylistItemsTool,
spotifyReplacePlaylistItemsTool,
spotifySaveAlbumsTool,
spotifySaveAudiobooksTool,
spotifySaveEpisodesTool,
spotifySaveShowsTool,
spotifySaveTracksTool,
spotifySearchTool,
spotifySeekTool,
spotifySetRepeatTool,
spotifySetShuffleTool,
spotifySetVolumeTool,
spotifySkipNextTool,
spotifySkipPreviousTool,
spotifyTransferPlaybackTool,
spotifyUnfollowArtistsTool,
spotifyUnfollowPlaylistTool,
spotifyUpdatePlaylistTool,
} from '@/tools/spotify'
import {
sshCheckCommandExistsTool,
sshCheckFileExistsTool,
@@ -2429,4 +2509,83 @@ export const tools: Record<string, ToolConfig> = {
zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool,
zoom_delete_recording: zoomDeleteRecordingTool,
zoom_list_past_participants: zoomListPastParticipantsTool,
// Spotify
spotify_search: spotifySearchTool,
spotify_get_track: spotifyGetTrackTool,
spotify_get_tracks: spotifyGetTracksTool,
spotify_get_album: spotifyGetAlbumTool,
spotify_get_albums: spotifyGetAlbumsTool,
spotify_get_album_tracks: spotifyGetAlbumTracksTool,
spotify_get_saved_albums: spotifyGetSavedAlbumsTool,
spotify_save_albums: spotifySaveAlbumsTool,
spotify_remove_saved_albums: spotifyRemoveSavedAlbumsTool,
spotify_check_saved_albums: spotifyCheckSavedAlbumsTool,
spotify_get_artist: spotifyGetArtistTool,
spotify_get_artists: spotifyGetArtistsTool,
spotify_get_artist_albums: spotifyGetArtistAlbumsTool,
spotify_get_artist_top_tracks: spotifyGetArtistTopTracksTool,
spotify_follow_artists: spotifyFollowArtistsTool,
spotify_unfollow_artists: spotifyUnfollowArtistsTool,
spotify_get_followed_artists: spotifyGetFollowedArtistsTool,
spotify_check_following: spotifyCheckFollowingTool,
spotify_get_show: spotifyGetShowTool,
spotify_get_shows: spotifyGetShowsTool,
spotify_get_show_episodes: spotifyGetShowEpisodesTool,
spotify_get_saved_shows: spotifyGetSavedShowsTool,
spotify_save_shows: spotifySaveShowsTool,
spotify_remove_saved_shows: spotifyRemoveSavedShowsTool,
spotify_check_saved_shows: spotifyCheckSavedShowsTool,
spotify_get_episode: spotifyGetEpisodeTool,
spotify_get_episodes: spotifyGetEpisodesTool,
spotify_get_saved_episodes: spotifyGetSavedEpisodesTool,
spotify_save_episodes: spotifySaveEpisodesTool,
spotify_remove_saved_episodes: spotifyRemoveSavedEpisodesTool,
spotify_check_saved_episodes: spotifyCheckSavedEpisodesTool,
spotify_get_audiobook: spotifyGetAudiobookTool,
spotify_get_audiobooks: spotifyGetAudiobooksTool,
spotify_get_audiobook_chapters: spotifyGetAudiobookChaptersTool,
spotify_get_saved_audiobooks: spotifyGetSavedAudiobooksTool,
spotify_save_audiobooks: spotifySaveAudiobooksTool,
spotify_remove_saved_audiobooks: spotifyRemoveSavedAudiobooksTool,
spotify_check_saved_audiobooks: spotifyCheckSavedAudiobooksTool,
spotify_get_playlist: spotifyGetPlaylistTool,
spotify_get_playlist_tracks: spotifyGetPlaylistTracksTool,
spotify_get_playlist_cover: spotifyGetPlaylistCoverTool,
spotify_get_user_playlists: spotifyGetUserPlaylistsTool,
spotify_create_playlist: spotifyCreatePlaylistTool,
spotify_update_playlist: spotifyUpdatePlaylistTool,
spotify_add_playlist_cover: spotifyAddPlaylistCoverTool,
spotify_add_tracks_to_playlist: spotifyAddTracksToPlaylistTool,
spotify_remove_tracks_from_playlist: spotifyRemoveTracksFromPlaylistTool,
spotify_reorder_playlist_items: spotifyReorderPlaylistItemsTool,
spotify_replace_playlist_items: spotifyReplacePlaylistItemsTool,
spotify_follow_playlist: spotifyFollowPlaylistTool,
spotify_unfollow_playlist: spotifyUnfollowPlaylistTool,
spotify_check_playlist_followers: spotifyCheckPlaylistFollowersTool,
spotify_get_current_user: spotifyGetCurrentUserTool,
spotify_get_user_profile: spotifyGetUserProfileTool,
spotify_get_top_tracks: spotifyGetTopTracksTool,
spotify_get_top_artists: spotifyGetTopArtistsTool,
spotify_get_saved_tracks: spotifyGetSavedTracksTool,
spotify_save_tracks: spotifySaveTracksTool,
spotify_remove_saved_tracks: spotifyRemoveSavedTracksTool,
spotify_check_saved_tracks: spotifyCheckSavedTracksTool,
spotify_get_recently_played: spotifyGetRecentlyPlayedTool,
spotify_get_new_releases: spotifyGetNewReleasesTool,
spotify_get_categories: spotifyGetCategoriesTool,
spotify_get_markets: spotifyGetMarketsTool,
spotify_get_playback_state: spotifyGetPlaybackStateTool,
spotify_get_currently_playing: spotifyGetCurrentlyPlayingTool,
spotify_get_devices: spotifyGetDevicesTool,
spotify_get_queue: spotifyGetQueueTool,
spotify_play: spotifyPlayTool,
spotify_pause: spotifyPauseTool,
spotify_skip_next: spotifySkipNextTool,
spotify_skip_previous: spotifySkipPreviousTool,
spotify_seek: spotifySeekTool,
spotify_add_to_queue: spotifyAddToQueueTool,
spotify_set_volume: spotifySetVolumeTool,
spotify_set_repeat: spotifySetRepeatTool,
spotify_set_shuffle: spotifySetShuffleTool,
spotify_transfer_playback: spotifyTransferPlaybackTool,
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyAddPlaylistCoverParams {
accessToken: string
playlistId: string
imageBase64: string
}
interface SpotifyAddPlaylistCoverResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyAddPlaylistCoverTool: ToolConfig<
SpotifyAddPlaylistCoverParams,
SpotifyAddPlaylistCoverResponse
> = {
id: 'spotify_add_playlist_cover',
name: 'Spotify Add Playlist Cover',
description: 'Upload a custom cover image for a playlist. Image must be JPEG and under 256KB.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private', 'ugc-image-upload'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
imageBase64: {
type: 'string',
required: true,
description: 'Base64-encoded JPEG image (max 256KB)',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/images`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'image/jpeg',
}),
body: (params) => params.imageBase64,
},
transformResponse: async (): Promise<SpotifyAddPlaylistCoverResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether upload succeeded' },
},
}

View File

@@ -0,0 +1,59 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyAddToQueueParams, SpotifyAddToQueueResponse } from './types'
export const spotifyAddToQueueTool: ToolConfig<SpotifyAddToQueueParams, SpotifyAddToQueueResponse> =
{
id: 'spotify_add_to_queue',
name: 'Spotify Add to Queue',
description: "Add a track to the user's playback queue.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
uri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Spotify URI of the track to add (e.g., "spotify:track:xxx")',
},
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID. If not provided, uses active device.',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/me/player/queue?uri=${encodeURIComponent(params.uri)}`
if (params.device_id) {
url += `&device_id=${params.device_id}`
}
return url
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifyAddToQueueResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether track was added to queue' },
},
}

View File

@@ -0,0 +1,71 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyAddTracksToPlaylistParams, SpotifyAddTracksToPlaylistResponse } from './types'
export const spotifyAddTracksToPlaylistTool: ToolConfig<
SpotifyAddTracksToPlaylistParams,
SpotifyAddTracksToPlaylistResponse
> = {
id: 'spotify_add_tracks_to_playlist',
name: 'Spotify Add Tracks to Playlist',
description: 'Add one or more tracks to a Spotify playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the playlist',
},
uris: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated Spotify URIs (e.g., "spotify:track:xxx,spotify:track:yyy")',
},
position: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Position to insert tracks (0-based). If omitted, tracks are appended.',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const uris = params.uris.split(',').map((uri) => uri.trim())
const body: any = { uris }
if (params.position !== undefined) {
body.position = params.position
}
return body
},
},
transformResponse: async (response): Promise<SpotifyAddTracksToPlaylistResponse> => {
const data = await response.json()
return {
success: true,
output: {
snapshot_id: data.snapshot_id,
},
}
},
outputs: {
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
},
}

View File

@@ -0,0 +1,64 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckFollowingParams {
accessToken: string
type: string
ids: string
}
interface SpotifyCheckFollowingResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckFollowingTool: ToolConfig<
SpotifyCheckFollowingParams,
SpotifyCheckFollowingResponse
> = {
id: 'spotify_check_following',
name: 'Spotify Check Following',
description: 'Check if the user follows artists or users.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-follow-read'],
},
params: {
type: {
type: 'string',
required: true,
description: 'Type to check: "artist" or "user"',
},
ids: {
type: 'string',
required: true,
description: 'Comma-separated artist or user IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.ids
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/following/contains?type=${params.type}&ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckFollowingResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each ID' },
},
}

View File

@@ -0,0 +1,64 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckPlaylistFollowersParams {
accessToken: string
playlistId: string
userIds: string
}
interface SpotifyCheckPlaylistFollowersResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckPlaylistFollowersTool: ToolConfig<
SpotifyCheckPlaylistFollowersParams,
SpotifyCheckPlaylistFollowersResponse
> = {
id: 'spotify_check_playlist_followers',
name: 'Spotify Check Playlist Followers',
description: 'Check if users follow a playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-read-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
userIds: {
type: 'string',
required: true,
description: 'Comma-separated user IDs to check (max 5)',
},
},
request: {
url: (params) => {
const ids = params.userIds
.split(',')
.map((id) => id.trim())
.slice(0, 5)
.join(',')
return `https://api.spotify.com/v1/playlists/${params.playlistId}/followers/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckPlaylistFollowersResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each user' },
},
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckSavedAlbumsParams {
accessToken: string
albumIds: string
}
interface SpotifyCheckSavedAlbumsResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckSavedAlbumsTool: ToolConfig<
SpotifyCheckSavedAlbumsParams,
SpotifyCheckSavedAlbumsResponse
> = {
id: 'spotify_check_saved_albums',
name: 'Spotify Check Saved Albums',
description: 'Check if albums are saved in library.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
albumIds: {
type: 'string',
required: true,
description: 'Comma-separated album IDs (max 20)',
},
},
request: {
url: (params) => {
const ids = params.albumIds
.split(',')
.map((id) => id.trim())
.slice(0, 20)
.join(',')
return `https://api.spotify.com/v1/me/albums/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckSavedAlbumsResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each album' },
},
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckSavedAudiobooksParams {
accessToken: string
audiobookIds: string
}
interface SpotifyCheckSavedAudiobooksResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckSavedAudiobooksTool: ToolConfig<
SpotifyCheckSavedAudiobooksParams,
SpotifyCheckSavedAudiobooksResponse
> = {
id: 'spotify_check_saved_audiobooks',
name: 'Spotify Check Saved Audiobooks',
description: 'Check if audiobooks are saved in library.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
audiobookIds: {
type: 'string',
required: true,
description: 'Comma-separated audiobook IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.audiobookIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/audiobooks/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckSavedAudiobooksResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each audiobook' },
},
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckSavedEpisodesParams {
accessToken: string
episodeIds: string
}
interface SpotifyCheckSavedEpisodesResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckSavedEpisodesTool: ToolConfig<
SpotifyCheckSavedEpisodesParams,
SpotifyCheckSavedEpisodesResponse
> = {
id: 'spotify_check_saved_episodes',
name: 'Spotify Check Saved Episodes',
description: 'Check if episodes are saved in library.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
episodeIds: {
type: 'string',
required: true,
description: 'Comma-separated episode IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.episodeIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/episodes/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckSavedEpisodesResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each episode' },
},
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyCheckSavedShowsParams {
accessToken: string
showIds: string
}
interface SpotifyCheckSavedShowsResponse extends ToolResponse {
output: { results: boolean[] }
}
export const spotifyCheckSavedShowsTool: ToolConfig<
SpotifyCheckSavedShowsParams,
SpotifyCheckSavedShowsResponse
> = {
id: 'spotify_check_saved_shows',
name: 'Spotify Check Saved Shows',
description: 'Check if shows are saved in library.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
showIds: {
type: 'string',
required: true,
description: 'Comma-separated show IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.showIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/shows/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyCheckSavedShowsResponse> => {
const results = await response.json()
return { success: true, output: { results } }
},
outputs: {
results: { type: 'json', description: 'Array of booleans for each show' },
},
}

View File

@@ -0,0 +1,71 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyCheckSavedTracksParams, SpotifyCheckSavedTracksResponse } from './types'
export const spotifyCheckSavedTracksTool: ToolConfig<
SpotifyCheckSavedTracksParams,
SpotifyCheckSavedTracksResponse
> = {
id: 'spotify_check_saved_tracks',
name: 'Spotify Check Saved Tracks',
description: "Check if one or more tracks are saved in the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
trackIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated track IDs to check (max 50)',
},
},
request: {
url: (params) => {
const ids = params.trackIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/tracks/contains?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params): Promise<SpotifyCheckSavedTracksResponse> => {
const data = await response.json()
const ids = (params?.trackIds || '')
.split(',')
.map((id) => id.trim())
.slice(0, 50)
const results = ids.map((id, index) => ({
id,
saved: data[index] || false,
}))
return {
success: true,
output: {
results,
all_saved: data.every((saved: boolean) => saved),
none_saved: data.every((saved: boolean) => !saved),
},
}
},
outputs: {
results: { type: 'json', description: 'Array of track IDs with saved status' },
all_saved: { type: 'boolean', description: 'Whether all tracks are saved' },
none_saved: { type: 'boolean', description: 'Whether no tracks are saved' },
},
}

View File

@@ -0,0 +1,89 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyCreatePlaylistParams, SpotifyCreatePlaylistResponse } from './types'
export const spotifyCreatePlaylistTool: ToolConfig<
SpotifyCreatePlaylistParams,
SpotifyCreatePlaylistResponse
> = {
id: 'spotify_create_playlist',
name: 'Spotify Create Playlist',
description: 'Create a new playlist for the current user on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new playlist',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description for the playlist',
},
public: {
type: 'boolean',
required: false,
visibility: 'user-only',
default: true,
description: 'Whether the playlist should be public',
},
collaborative: {
type: 'boolean',
required: false,
visibility: 'user-only',
default: false,
description: 'Whether the playlist should be collaborative (requires public to be false)',
},
},
request: {
url: () => 'https://api.spotify.com/v1/me/playlists',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
name: params.name,
description: params.description || '',
public: params.public !== false,
collaborative: params.collaborative === true,
}),
},
transformResponse: async (response): Promise<SpotifyCreatePlaylistResponse> => {
const playlist = await response.json()
return {
success: true,
output: {
id: playlist.id,
name: playlist.name,
description: playlist.description,
public: playlist.public,
collaborative: playlist.collaborative,
snapshot_id: playlist.snapshot_id,
external_url: playlist.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify playlist ID' },
name: { type: 'string', description: 'Playlist name' },
description: { type: 'string', description: 'Playlist description', optional: true },
public: { type: 'boolean', description: 'Whether the playlist is public' },
collaborative: { type: 'boolean', description: 'Whether collaborative' },
snapshot_id: { type: 'string', description: 'Playlist snapshot ID' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,64 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyFollowArtistsParams {
accessToken: string
artistIds: string
}
interface SpotifyFollowArtistsResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifyFollowArtistsTool: ToolConfig<
SpotifyFollowArtistsParams,
SpotifyFollowArtistsResponse
> = {
id: 'spotify_follow_artists',
name: 'Spotify Follow Artists',
description: 'Follow one or more artists.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-follow-modify'],
},
params: {
artistIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated artist IDs to follow (max 50)',
},
},
request: {
url: (params) => {
const ids = params.artistIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifyFollowArtistsResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether artists were followed successfully' },
},
}

View File

@@ -0,0 +1,61 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyFollowPlaylistParams {
accessToken: string
playlistId: string
public?: boolean
}
interface SpotifyFollowPlaylistResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyFollowPlaylistTool: ToolConfig<
SpotifyFollowPlaylistParams,
SpotifyFollowPlaylistResponse
> = {
id: 'spotify_follow_playlist',
name: 'Spotify Follow Playlist',
description: 'Follow (save) a playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
public: {
type: 'boolean',
required: false,
default: true,
description: 'Whether the playlist will be in public playlists',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/followers`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
public: params.public ?? true,
}),
},
transformResponse: async (): Promise<SpotifyFollowPlaylistResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether follow succeeded' },
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetAlbumParams, SpotifyGetAlbumResponse } from './types'
export const spotifyGetAlbumTool: ToolConfig<SpotifyGetAlbumParams, SpotifyGetAlbumResponse> = {
id: 'spotify_get_album',
name: 'Spotify Get Album',
description:
'Get detailed information about an album on Spotify by its ID, including track listing.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
albumId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the album',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code for track availability',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/albums/${params.albumId}`
if (params.market) {
url += `?market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetAlbumResponse> => {
const album = await response.json()
return {
success: true,
output: {
id: album.id,
name: album.name,
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album_type: album.album_type,
total_tracks: album.total_tracks,
release_date: album.release_date,
label: album.label || '',
popularity: album.popularity,
genres: album.genres || [],
image_url: album.images?.[0]?.url || null,
tracks: (album.tracks?.items || []).map((t: any) => ({
id: t.id,
name: t.name,
duration_ms: t.duration_ms,
track_number: t.track_number,
})),
external_url: album.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify album ID' },
name: { type: 'string', description: 'Album name' },
artists: { type: 'array', description: 'List of artists' },
album_type: { type: 'string', description: 'Type of album (album, single, compilation)' },
total_tracks: { type: 'number', description: 'Total number of tracks' },
release_date: { type: 'string', description: 'Release date' },
label: { type: 'string', description: 'Record label' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
genres: { type: 'array', description: 'List of genres' },
image_url: { type: 'string', description: 'Album cover image URL', optional: true },
tracks: { type: 'array', description: 'List of tracks on the album' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,112 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetAlbumTracksParams {
accessToken: string
albumId: string
limit?: number
offset?: number
market?: string
}
interface SpotifyGetAlbumTracksResponse extends ToolResponse {
output: {
tracks: Array<{
id: string
name: string
artists: Array<{ id: string; name: string }>
duration_ms: number
track_number: number
disc_number: number
explicit: boolean
preview_url: string | null
}>
total: number
next: string | null
}
}
export const spotifyGetAlbumTracksTool: ToolConfig<
SpotifyGetAlbumTracksParams,
SpotifyGetAlbumTracksResponse
> = {
id: 'spotify_get_album_tracks',
name: 'Spotify Get Album Tracks',
description: 'Get the tracks from an album.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
albumId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify album ID',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of tracks to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of first track to return',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/albums/${params.albumId}/tracks?limit=${limit}&offset=${offset}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetAlbumTracksResponse> => {
const data = await response.json()
const tracks = (data.items || []).map((track: any) => ({
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
duration_ms: track.duration_ms,
track_number: track.track_number,
disc_number: track.disc_number,
explicit: track.explicit || false,
preview_url: track.preview_url || null,
}))
return {
success: true,
output: {
tracks,
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
tracks: { type: 'json', description: 'List of tracks' },
total: { type: 'number', description: 'Total number of tracks' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,94 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetAlbumsParams {
accessToken: string
albumIds: string
market?: string
}
interface SpotifyGetAlbumsResponse extends ToolResponse {
output: {
albums: Array<{
id: string
name: string
artists: Array<{ id: string; name: string }>
album_type: string
total_tracks: number
release_date: string
image_url: string | null
external_url: string
}>
}
}
export const spotifyGetAlbumsTool: ToolConfig<SpotifyGetAlbumsParams, SpotifyGetAlbumsResponse> = {
id: 'spotify_get_albums',
name: 'Spotify Get Multiple Albums',
description: 'Get details for multiple albums by their IDs.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
albumIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated album IDs (max 20)',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const ids = params.albumIds
.split(',')
.map((id) => id.trim())
.slice(0, 20)
.join(',')
let url = `https://api.spotify.com/v1/albums?ids=${ids}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetAlbumsResponse> => {
const data = await response.json()
const albums = (data.albums || []).map((album: any) => ({
id: album.id,
name: album.name,
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album_type: album.album_type,
total_tracks: album.total_tracks,
release_date: album.release_date,
image_url: album.images?.[0]?.url || null,
external_url: album.external_urls?.spotify || '',
}))
return {
success: true,
output: { albums },
}
},
outputs: {
albums: { type: 'json', description: 'List of albums' },
},
}

View File

@@ -0,0 +1,59 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetArtistParams, SpotifyGetArtistResponse } from './types'
export const spotifyGetArtistTool: ToolConfig<SpotifyGetArtistParams, SpotifyGetArtistResponse> = {
id: 'spotify_get_artist',
name: 'Spotify Get Artist',
description: 'Get detailed information about an artist on Spotify by their ID.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
artistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the artist',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/artists/${params.artistId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetArtistResponse> => {
const artist = await response.json()
return {
success: true,
output: {
id: artist.id,
name: artist.name,
genres: artist.genres || [],
popularity: artist.popularity,
followers: artist.followers?.total || 0,
image_url: artist.images?.[0]?.url || null,
external_url: artist.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify artist ID' },
name: { type: 'string', description: 'Artist name' },
genres: { type: 'array', description: 'List of genres associated with the artist' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
followers: { type: 'number', description: 'Number of followers' },
image_url: { type: 'string', description: 'Artist image URL', optional: true },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,116 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetArtistAlbumsParams, SpotifyGetArtistAlbumsResponse } from './types'
export const spotifyGetArtistAlbumsTool: ToolConfig<
SpotifyGetArtistAlbumsParams,
SpotifyGetArtistAlbumsResponse
> = {
id: 'spotify_get_artist_albums',
name: 'Spotify Get Artist Albums',
description: 'Get albums by an artist on Spotify. Can filter by album type.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
artistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the artist',
},
include_groups: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by album type: album, single, appears_on, compilation (comma-separated)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Maximum number of albums to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first album to return',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/artists/${params.artistId}/albums?limit=${limit}&offset=${offset}`
if (params.include_groups) {
url += `&include_groups=${encodeURIComponent(params.include_groups)}`
}
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetArtistAlbumsResponse> => {
const data = await response.json()
const albums = (data.items || []).map((album: any) => ({
id: album.id,
name: album.name,
album_type: album.album_type,
total_tracks: album.total_tracks,
release_date: album.release_date,
image_url: album.images?.[0]?.url || null,
external_url: album.external_urls?.spotify || '',
}))
return {
success: true,
output: {
albums,
total: data.total || albums.length,
next: data.next || null,
},
}
},
outputs: {
albums: {
type: 'array',
description: "Artist's albums",
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify album ID' },
name: { type: 'string', description: 'Album name' },
album_type: { type: 'string', description: 'Type (album, single, compilation)' },
total_tracks: { type: 'number', description: 'Number of tracks' },
release_date: { type: 'string', description: 'Release date' },
image_url: { type: 'string', description: 'Album cover URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
total: { type: 'number', description: 'Total number of albums available' },
next: { type: 'string', description: 'URL for next page of results', optional: true },
},
}

View File

@@ -0,0 +1,89 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetArtistTopTracksParams, SpotifyGetArtistTopTracksResponse } from './types'
export const spotifyGetArtistTopTracksTool: ToolConfig<
SpotifyGetArtistTopTracksParams,
SpotifyGetArtistTopTracksResponse
> = {
id: 'spotify_get_artist_top_tracks',
name: 'Spotify Get Artist Top Tracks',
description: 'Get the top 10 most popular tracks by an artist on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
artistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the artist',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
default: 'US',
description: 'ISO 3166-1 alpha-2 country code (required for this endpoint)',
},
},
request: {
url: (params) => {
const market = params.market || 'US'
return `https://api.spotify.com/v1/artists/${params.artistId}/top-tracks?market=${market}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetArtistTopTracksResponse> => {
const data = await response.json()
const tracks = (data.tracks || []).map((track: any) => ({
id: track.id,
name: track.name,
album: {
id: track.album?.id || '',
name: track.album?.name || '',
image_url: track.album?.images?.[0]?.url || null,
},
duration_ms: track.duration_ms,
popularity: track.popularity,
preview_url: track.preview_url,
external_url: track.external_urls?.spotify || '',
}))
return {
success: true,
output: {
tracks,
},
}
},
outputs: {
tracks: {
type: 'array',
description: "Artist's top tracks",
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify track ID' },
name: { type: 'string', description: 'Track name' },
album: { type: 'object', description: 'Album information' },
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
preview_url: { type: 'string', description: 'URL to 30-second preview' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
},
}

View File

@@ -0,0 +1,82 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetArtistsParams {
accessToken: string
artistIds: string
}
interface SpotifyGetArtistsResponse extends ToolResponse {
output: {
artists: Array<{
id: string
name: string
genres: string[]
popularity: number
followers: number
image_url: string | null
external_url: string
}>
}
}
export const spotifyGetArtistsTool: ToolConfig<SpotifyGetArtistsParams, SpotifyGetArtistsResponse> =
{
id: 'spotify_get_artists',
name: 'Spotify Get Multiple Artists',
description: 'Get details for multiple artists by their IDs.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
artistIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated artist IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.artistIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/artists?ids=${ids}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetArtistsResponse> => {
const data = await response.json()
const artists = (data.artists || []).map((artist: any) => ({
id: artist.id,
name: artist.name,
genres: artist.genres || [],
popularity: artist.popularity || 0,
followers: artist.followers?.total || 0,
image_url: artist.images?.[0]?.url || null,
external_url: artist.external_urls?.spotify || '',
}))
return {
success: true,
output: { artists },
}
},
outputs: {
artists: { type: 'json', description: 'List of artists' },
},
}

View File

@@ -0,0 +1,95 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetAudiobookParams {
accessToken: string
audiobookId: string
market?: string
}
interface SpotifyGetAudiobookResponse extends ToolResponse {
output: {
id: string
name: string
authors: Array<{ name: string }>
narrators: Array<{ name: string }>
publisher: string
description: string
total_chapters: number
languages: string[]
image_url: string | null
external_url: string
}
}
export const spotifyGetAudiobookTool: ToolConfig<
SpotifyGetAudiobookParams,
SpotifyGetAudiobookResponse
> = {
id: 'spotify_get_audiobook',
name: 'Spotify Get Audiobook',
description: 'Get details for an audiobook.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
audiobookId: {
type: 'string',
required: true,
description: 'The Spotify audiobook ID',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/audiobooks/${params.audiobookId}`
if (params.market) url += `?market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetAudiobookResponse> => {
const book = await response.json()
return {
success: true,
output: {
id: book.id,
name: book.name,
authors: book.authors || [],
narrators: book.narrators || [],
publisher: book.publisher || '',
description: book.description || '',
total_chapters: book.total_chapters || 0,
languages: book.languages || [],
image_url: book.images?.[0]?.url || null,
external_url: book.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Audiobook ID' },
name: { type: 'string', description: 'Audiobook name' },
authors: { type: 'json', description: 'Authors' },
narrators: { type: 'json', description: 'Narrators' },
publisher: { type: 'string', description: 'Publisher' },
description: { type: 'string', description: 'Description' },
total_chapters: { type: 'number', description: 'Total chapters' },
languages: { type: 'json', description: 'Languages' },
image_url: { type: 'string', description: 'Cover image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,104 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetAudiobookChaptersParams {
accessToken: string
audiobookId: string
limit?: number
offset?: number
market?: string
}
interface SpotifyGetAudiobookChaptersResponse extends ToolResponse {
output: {
chapters: Array<{
id: string
name: string
chapter_number: number
duration_ms: number
image_url: string | null
external_url: string
}>
total: number
next: string | null
}
}
export const spotifyGetAudiobookChaptersTool: ToolConfig<
SpotifyGetAudiobookChaptersParams,
SpotifyGetAudiobookChaptersResponse
> = {
id: 'spotify_get_audiobook_chapters',
name: 'Spotify Get Audiobook Chapters',
description: 'Get chapters from an audiobook.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
audiobookId: {
type: 'string',
required: true,
description: 'The Spotify audiobook ID',
},
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of chapters to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first chapter to return',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/audiobooks/${params.audiobookId}/chapters?limit=${limit}&offset=${offset}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetAudiobookChaptersResponse> => {
const data = await response.json()
return {
success: true,
output: {
chapters: (data.items || []).map((ch: any) => ({
id: ch.id,
name: ch.name,
chapter_number: ch.chapter_number || 0,
duration_ms: ch.duration_ms || 0,
image_url: ch.images?.[0]?.url || null,
external_url: ch.external_urls?.spotify || '',
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
chapters: { type: 'json', description: 'List of chapters' },
total: { type: 'number', description: 'Total chapters' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetAudiobooksParams {
accessToken: string
audiobookIds: string
market?: string
}
interface SpotifyGetAudiobooksResponse extends ToolResponse {
output: {
audiobooks: Array<{
id: string
name: string
authors: Array<{ name: string }>
total_chapters: number
image_url: string | null
external_url: string
}>
}
}
export const spotifyGetAudiobooksTool: ToolConfig<
SpotifyGetAudiobooksParams,
SpotifyGetAudiobooksResponse
> = {
id: 'spotify_get_audiobooks',
name: 'Spotify Get Multiple Audiobooks',
description: 'Get details for multiple audiobooks.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
audiobookIds: {
type: 'string',
required: true,
description: 'Comma-separated audiobook IDs (max 50)',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const ids = params.audiobookIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
let url = `https://api.spotify.com/v1/audiobooks?ids=${ids}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetAudiobooksResponse> => {
const data = await response.json()
return {
success: true,
output: {
audiobooks: (data.audiobooks || []).map((book: any) => ({
id: book.id,
name: book.name,
authors: book.authors || [],
total_chapters: book.total_chapters || 0,
image_url: book.images?.[0]?.url || null,
external_url: book.external_urls?.spotify || '',
})),
},
}
},
outputs: {
audiobooks: { type: 'json', description: 'List of audiobooks' },
},
}

View File

@@ -0,0 +1,82 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetCategoriesParams, SpotifyGetCategoriesResponse } from './types'
export const spotifyGetCategoriesTool: ToolConfig<
SpotifyGetCategoriesParams,
SpotifyGetCategoriesResponse
> = {
id: 'spotify_get_categories',
name: 'Spotify Get Categories',
description: 'Get a list of browse categories used to tag items in Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
country: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
},
locale: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Locale code (e.g., "en_US", "es_MX")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of categories to return (1-50)',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
let url = `https://api.spotify.com/v1/browse/categories?limit=${limit}`
if (params.country) {
url += `&country=${params.country}`
}
if (params.locale) {
url += `&locale=${params.locale}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetCategoriesResponse> => {
const data = await response.json()
const categories = (data.categories?.items || []).map((category: any) => ({
id: category.id,
name: category.name,
icon_url: category.icons?.[0]?.url || null,
}))
return {
success: true,
output: {
categories,
total: data.categories?.total || 0,
},
}
},
outputs: {
categories: { type: 'json', description: 'List of browse categories' },
total: { type: 'number', description: 'Total number of categories' },
},
}

View File

@@ -0,0 +1,58 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetCurrentUserParams, SpotifyGetCurrentUserResponse } from './types'
export const spotifyGetCurrentUserTool: ToolConfig<
SpotifyGetCurrentUserParams,
SpotifyGetCurrentUserResponse
> = {
id: 'spotify_get_current_user',
name: 'Spotify Get Current User',
description: "Get the current user's Spotify profile information.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private', 'user-read-email'],
},
params: {},
request: {
url: () => 'https://api.spotify.com/v1/me',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetCurrentUserResponse> => {
const user = await response.json()
return {
success: true,
output: {
id: user.id,
display_name: user.display_name || '',
email: user.email || null,
country: user.country || null,
product: user.product || null,
followers: user.followers?.total || 0,
image_url: user.images?.[0]?.url || null,
external_url: user.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify user ID' },
display_name: { type: 'string', description: 'Display name' },
email: { type: 'string', description: 'Email address', optional: true },
country: { type: 'string', description: 'Country code', optional: true },
product: { type: 'string', description: 'Subscription level (free, premium)', optional: true },
followers: { type: 'number', description: 'Number of followers' },
image_url: { type: 'string', description: 'Profile image URL', optional: true },
external_url: { type: 'string', description: 'Spotify profile URL' },
},
}

View File

@@ -0,0 +1,108 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetCurrentlyPlayingParams {
accessToken: string
market?: string
}
interface SpotifyGetCurrentlyPlayingResponse extends ToolResponse {
output: {
is_playing: boolean
progress_ms: number | null
track: {
id: string
name: string
artists: Array<{ id: string; name: string }>
album: {
id: string
name: string
image_url: string | null
}
duration_ms: number
external_url: string
} | null
}
}
export const spotifyGetCurrentlyPlayingTool: ToolConfig<
SpotifyGetCurrentlyPlayingParams,
SpotifyGetCurrentlyPlayingResponse
> = {
id: 'spotify_get_currently_playing',
name: 'Spotify Get Currently Playing',
description: "Get the user's currently playing track.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-currently-playing'],
},
params: {
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player/currently-playing'
if (params.market) {
url += `?market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetCurrentlyPlayingResponse> => {
if (response.status === 204) {
return {
success: true,
output: {
is_playing: false,
progress_ms: null,
track: null,
},
}
}
const data = await response.json()
return {
success: true,
output: {
is_playing: data.is_playing || false,
progress_ms: data.progress_ms || null,
track: data.item
? {
id: data.item.id,
name: data.item.name,
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: data.item.album?.id || '',
name: data.item.album?.name || '',
image_url: data.item.album?.images?.[0]?.url || null,
},
duration_ms: data.item.duration_ms,
external_url: data.item.external_urls?.spotify || '',
}
: null,
},
}
},
outputs: {
is_playing: { type: 'boolean', description: 'Whether playback is active' },
progress_ms: { type: 'number', description: 'Current position in track (ms)', optional: true },
track: { type: 'json', description: 'Currently playing track', optional: true },
},
}

View File

@@ -0,0 +1,67 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetDevicesParams, SpotifyGetDevicesResponse } from './types'
export const spotifyGetDevicesTool: ToolConfig<SpotifyGetDevicesParams, SpotifyGetDevicesResponse> =
{
id: 'spotify_get_devices',
name: 'Spotify Get Devices',
description: "Get the user's available Spotify playback devices.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-state'],
},
params: {},
request: {
url: () => 'https://api.spotify.com/v1/me/player/devices',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetDevicesResponse> => {
const data = await response.json()
const devices = (data.devices || []).map((device: any) => ({
id: device.id,
is_active: device.is_active,
is_private_session: device.is_private_session,
is_restricted: device.is_restricted,
name: device.name,
type: device.type,
volume_percent: device.volume_percent,
}))
return {
success: true,
output: {
devices,
},
}
},
outputs: {
devices: {
type: 'array',
description: 'Available playback devices',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Device ID' },
is_active: { type: 'boolean', description: 'Whether device is active' },
is_private_session: { type: 'boolean', description: 'Whether in private session' },
is_restricted: { type: 'boolean', description: 'Whether device is restricted' },
name: { type: 'string', description: 'Device name' },
type: { type: 'string', description: 'Device type (Computer, Smartphone, etc.)' },
volume_percent: { type: 'number', description: 'Current volume (0-100)' },
},
},
},
},
}

View File

@@ -0,0 +1,94 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetEpisodeParams {
accessToken: string
episodeId: string
market?: string
}
interface SpotifyGetEpisodeResponse extends ToolResponse {
output: {
id: string
name: string
description: string
duration_ms: number
release_date: string
explicit: boolean
show: { id: string; name: string; publisher: string }
image_url: string | null
external_url: string
}
}
export const spotifyGetEpisodeTool: ToolConfig<SpotifyGetEpisodeParams, SpotifyGetEpisodeResponse> =
{
id: 'spotify_get_episode',
name: 'Spotify Get Episode',
description: 'Get details for a podcast episode.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
episodeId: {
type: 'string',
required: true,
description: 'The Spotify episode ID',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/episodes/${params.episodeId}`
if (params.market) url += `?market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetEpisodeResponse> => {
const ep = await response.json()
return {
success: true,
output: {
id: ep.id,
name: ep.name,
description: ep.description || '',
duration_ms: ep.duration_ms || 0,
release_date: ep.release_date || '',
explicit: ep.explicit || false,
show: {
id: ep.show?.id || '',
name: ep.show?.name || '',
publisher: ep.show?.publisher || '',
},
image_url: ep.images?.[0]?.url || null,
external_url: ep.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Episode ID' },
name: { type: 'string', description: 'Episode name' },
description: { type: 'string', description: 'Episode description' },
duration_ms: { type: 'number', description: 'Duration in ms' },
release_date: { type: 'string', description: 'Release date' },
explicit: { type: 'boolean', description: 'Contains explicit content' },
show: { type: 'json', description: 'Parent show info' },
image_url: { type: 'string', description: 'Cover image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,91 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetEpisodesParams {
accessToken: string
episodeIds: string
market?: string
}
interface SpotifyGetEpisodesResponse extends ToolResponse {
output: {
episodes: Array<{
id: string
name: string
description: string
duration_ms: number
release_date: string
show: { id: string; name: string }
image_url: string | null
external_url: string
}>
}
}
export const spotifyGetEpisodesTool: ToolConfig<
SpotifyGetEpisodesParams,
SpotifyGetEpisodesResponse
> = {
id: 'spotify_get_episodes',
name: 'Spotify Get Multiple Episodes',
description: 'Get details for multiple podcast episodes.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
episodeIds: {
type: 'string',
required: true,
description: 'Comma-separated episode IDs (max 50)',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const ids = params.episodeIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
let url = `https://api.spotify.com/v1/episodes?ids=${ids}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetEpisodesResponse> => {
const data = await response.json()
return {
success: true,
output: {
episodes: (data.episodes || []).map((ep: any) => ({
id: ep.id,
name: ep.name,
description: ep.description || '',
duration_ms: ep.duration_ms || 0,
release_date: ep.release_date || '',
show: { id: ep.show?.id || '', name: ep.show?.name || '' },
image_url: ep.images?.[0]?.url || null,
external_url: ep.external_urls?.spotify || '',
})),
},
}
},
outputs: {
episodes: { type: 'json', description: 'List of episodes' },
},
}

View File

@@ -0,0 +1,100 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetFollowedArtistsParams {
accessToken: string
limit?: number
after?: string
}
interface SpotifyGetFollowedArtistsResponse extends ToolResponse {
output: {
artists: Array<{
id: string
name: string
genres: string[]
popularity: number
followers: number
image_url: string | null
external_url: string
}>
total: number
next: string | null
}
}
export const spotifyGetFollowedArtistsTool: ToolConfig<
SpotifyGetFollowedArtistsParams,
SpotifyGetFollowedArtistsResponse
> = {
id: 'spotify_get_followed_artists',
name: 'Spotify Get Followed Artists',
description: "Get the user's followed artists.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-follow-read'],
},
params: {
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of artists to return (1-50)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Cursor for pagination (last artist ID from previous request)',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
let url = `https://api.spotify.com/v1/me/following?type=artist&limit=${limit}`
if (params.after) {
url += `&after=${params.after}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetFollowedArtistsResponse> => {
const data = await response.json()
const artists = (data.artists?.items || []).map((artist: any) => ({
id: artist.id,
name: artist.name,
genres: artist.genres || [],
popularity: artist.popularity || 0,
followers: artist.followers?.total || 0,
image_url: artist.images?.[0]?.url || null,
external_url: artist.external_urls?.spotify || '',
}))
return {
success: true,
output: {
artists,
total: data.artists?.total || 0,
next: data.artists?.cursors?.after || null,
},
}
},
outputs: {
artists: { type: 'json', description: 'List of followed artists' },
total: { type: 'number', description: 'Total number of followed artists' },
next: { type: 'string', description: 'Cursor for next page', optional: true },
},
}

View File

@@ -0,0 +1,47 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetMarketsParams {
accessToken: string
}
interface SpotifyGetMarketsResponse extends ToolResponse {
output: {
markets: string[]
}
}
export const spotifyGetMarketsTool: ToolConfig<SpotifyGetMarketsParams, SpotifyGetMarketsResponse> =
{
id: 'spotify_get_markets',
name: 'Spotify Get Available Markets',
description: 'Get the list of markets where Spotify is available.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {},
request: {
url: () => 'https://api.spotify.com/v1/markets',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetMarketsResponse> => {
const data = await response.json()
return {
success: true,
output: { markets: data.markets || [] },
}
},
outputs: {
markets: { type: 'json', description: 'List of ISO country codes' },
},
}

View File

@@ -0,0 +1,88 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetNewReleasesParams, SpotifyGetNewReleasesResponse } from './types'
export const spotifyGetNewReleasesTool: ToolConfig<
SpotifyGetNewReleasesParams,
SpotifyGetNewReleasesResponse
> = {
id: 'spotify_get_new_releases',
name: 'Spotify Get New Releases',
description: 'Get a list of new album releases featured in Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
country: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of releases to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of first release to return',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/browse/new-releases?limit=${limit}&offset=${offset}`
if (params.country) {
url += `&country=${params.country}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetNewReleasesResponse> => {
const data = await response.json()
const albums = (data.albums?.items || []).map((album: any) => ({
id: album.id,
name: album.name,
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
release_date: album.release_date,
total_tracks: album.total_tracks,
album_type: album.album_type,
image_url: album.images?.[0]?.url || null,
external_url: album.external_urls?.spotify || '',
}))
return {
success: true,
output: {
albums,
total: data.albums?.total || 0,
next: data.albums?.next || null,
},
}
},
outputs: {
albums: { type: 'json', description: 'List of new releases' },
total: { type: 'number', description: 'Total number of new releases' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,103 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetPlaybackStateParams, SpotifyGetPlaybackStateResponse } from './types'
export const spotifyGetPlaybackStateTool: ToolConfig<
SpotifyGetPlaybackStateParams,
SpotifyGetPlaybackStateResponse
> = {
id: 'spotify_get_playback_state',
name: 'Spotify Get Playback State',
description: 'Get the current playback state including device, track, and progress.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-state'],
},
params: {
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player'
if (params.market) {
url += `?market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetPlaybackStateResponse> => {
if (response.status === 204) {
return {
success: true,
output: {
is_playing: false,
device: null,
progress_ms: null,
currently_playing_type: 'unknown',
shuffle_state: false,
repeat_state: 'off',
track: null,
},
}
}
const data = await response.json()
return {
success: true,
output: {
is_playing: data.is_playing || false,
device: data.device
? {
id: data.device.id,
name: data.device.name,
type: data.device.type,
volume_percent: data.device.volume_percent,
}
: null,
progress_ms: data.progress_ms,
currently_playing_type: data.currently_playing_type || 'unknown',
shuffle_state: data.shuffle_state || false,
repeat_state: data.repeat_state || 'off',
track: data.item
? {
id: data.item.id,
name: data.item.name,
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: data.item.album?.id || '',
name: data.item.album?.name || '',
image_url: data.item.album?.images?.[0]?.url || null,
},
duration_ms: data.item.duration_ms,
}
: null,
},
}
},
outputs: {
is_playing: { type: 'boolean', description: 'Whether playback is active' },
device: { type: 'object', description: 'Active device information', optional: true },
progress_ms: { type: 'number', description: 'Progress in milliseconds', optional: true },
currently_playing_type: { type: 'string', description: 'Type of content playing' },
shuffle_state: { type: 'boolean', description: 'Whether shuffle is enabled' },
repeat_state: { type: 'string', description: 'Repeat mode (off, track, context)' },
track: { type: 'object', description: 'Currently playing track', optional: true },
},
}

View File

@@ -0,0 +1,83 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetPlaylistParams, SpotifyGetPlaylistResponse } from './types'
export const spotifyGetPlaylistTool: ToolConfig<
SpotifyGetPlaylistParams,
SpotifyGetPlaylistResponse
> = {
id: 'spotify_get_playlist',
name: 'Spotify Get Playlist',
description: 'Get detailed information about a playlist on Spotify by its ID.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
playlistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the playlist',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code for track availability',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/playlists/${params.playlistId}`
if (params.market) {
url += `?market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetPlaylistResponse> => {
const playlist = await response.json()
return {
success: true,
output: {
id: playlist.id,
name: playlist.name,
description: playlist.description,
public: playlist.public,
collaborative: playlist.collaborative,
owner: {
id: playlist.owner?.id || '',
display_name: playlist.owner?.display_name || '',
},
image_url: playlist.images?.[0]?.url || null,
total_tracks: playlist.tracks?.total || 0,
snapshot_id: playlist.snapshot_id,
external_url: playlist.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify playlist ID' },
name: { type: 'string', description: 'Playlist name' },
description: { type: 'string', description: 'Playlist description', optional: true },
public: { type: 'boolean', description: 'Whether the playlist is public' },
collaborative: { type: 'boolean', description: 'Whether the playlist is collaborative' },
owner: { type: 'object', description: 'Playlist owner information' },
image_url: { type: 'string', description: 'Playlist cover image URL', optional: true },
total_tracks: { type: 'number', description: 'Total number of tracks' },
snapshot_id: { type: 'string', description: 'Playlist snapshot ID for versioning' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,66 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetPlaylistCoverParams {
accessToken: string
playlistId: string
}
interface SpotifyGetPlaylistCoverResponse extends ToolResponse {
output: {
images: Array<{
url: string
width: number | null
height: number | null
}>
}
}
export const spotifyGetPlaylistCoverTool: ToolConfig<
SpotifyGetPlaylistCoverParams,
SpotifyGetPlaylistCoverResponse
> = {
id: 'spotify_get_playlist_cover',
name: 'Spotify Get Playlist Cover',
description: "Get a playlist's cover image.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-read-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/images`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetPlaylistCoverResponse> => {
const images = await response.json()
return {
success: true,
output: {
images: (images || []).map((img: any) => ({
url: img.url,
width: img.width || null,
height: img.height || null,
})),
},
}
},
outputs: {
images: { type: 'json', description: 'List of cover images' },
},
}

View File

@@ -0,0 +1,113 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetPlaylistTracksParams, SpotifyGetPlaylistTracksResponse } from './types'
export const spotifyGetPlaylistTracksTool: ToolConfig<
SpotifyGetPlaylistTracksParams,
SpotifyGetPlaylistTracksResponse
> = {
id: 'spotify_get_playlist_tracks',
name: 'Spotify Get Playlist Tracks',
description: 'Get the tracks in a Spotify playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
playlistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the playlist',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 50,
description: 'Maximum number of tracks to return (1-100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first track to return',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code for track availability',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 50, 1), 100)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks?limit=${limit}&offset=${offset}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetPlaylistTracksResponse> => {
const data = await response.json()
const tracks = (data.items || [])
.filter((item: any) => item.track !== null)
.map((item: any) => ({
added_at: item.added_at,
added_by: item.added_by?.id || '',
track: {
id: item.track.id,
name: item.track.name,
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: item.track.album?.id || '',
name: item.track.album?.name || '',
image_url: item.track.album?.images?.[0]?.url || null,
},
duration_ms: item.track.duration_ms,
popularity: item.track.popularity,
external_url: item.track.external_urls?.spotify || '',
},
}))
return {
success: true,
output: {
tracks,
total: data.total || tracks.length,
next: data.next || null,
},
}
},
outputs: {
tracks: {
type: 'array',
description: 'List of tracks in the playlist',
items: {
type: 'object',
properties: {
added_at: { type: 'string', description: 'When the track was added' },
added_by: { type: 'string', description: 'User ID who added the track' },
track: { type: 'object', description: 'Track information' },
},
},
},
total: { type: 'number', description: 'Total number of tracks in playlist' },
next: { type: 'string', description: 'URL for next page of results', optional: true },
},
}

View File

@@ -0,0 +1,85 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetQueueParams {
accessToken: string
}
interface SpotifyGetQueueResponse extends ToolResponse {
output: {
currently_playing: {
id: string
name: string
artists: Array<{ id: string; name: string }>
album: {
id: string
name: string
image_url: string | null
}
duration_ms: number
} | null
queue: Array<{
id: string
name: string
artists: Array<{ id: string; name: string }>
album: {
id: string
name: string
image_url: string | null
}
duration_ms: number
}>
}
}
export const spotifyGetQueueTool: ToolConfig<SpotifyGetQueueParams, SpotifyGetQueueResponse> = {
id: 'spotify_get_queue',
name: 'Spotify Get Queue',
description: "Get the user's playback queue.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-state'],
},
params: {},
request: {
url: () => 'https://api.spotify.com/v1/me/player/queue',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetQueueResponse> => {
const data = await response.json()
const formatTrack = (track: any) => ({
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: track.album?.id || '',
name: track.album?.name || '',
image_url: track.album?.images?.[0]?.url || null,
},
duration_ms: track.duration_ms,
})
return {
success: true,
output: {
currently_playing: data.currently_playing ? formatTrack(data.currently_playing) : null,
queue: (data.queue || []).map(formatTrack),
},
}
},
outputs: {
currently_playing: { type: 'json', description: 'Currently playing track', optional: true },
queue: { type: 'json', description: 'Upcoming tracks in queue' },
},
}

View File

@@ -0,0 +1,102 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetRecentlyPlayedParams, SpotifyGetRecentlyPlayedResponse } from './types'
export const spotifyGetRecentlyPlayedTool: ToolConfig<
SpotifyGetRecentlyPlayedParams,
SpotifyGetRecentlyPlayedResponse
> = {
id: 'spotify_get_recently_played',
name: 'Spotify Get Recently Played',
description: "Get the user's recently played tracks.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-recently-played'],
},
params: {
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of tracks to return (1-50)',
},
after: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Unix timestamp in milliseconds. Returns items after this cursor.',
},
before: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Unix timestamp in milliseconds. Returns items before this cursor.',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
let url = `https://api.spotify.com/v1/me/player/recently-played?limit=${limit}`
if (params.after) {
url += `&after=${params.after}`
}
if (params.before) {
url += `&before=${params.before}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetRecentlyPlayedResponse> => {
const data = await response.json()
const items = (data.items || []).map((item: any) => ({
played_at: item.played_at,
track: {
id: item.track.id,
name: item.track.name,
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: item.track.album?.id || '',
name: item.track.album?.name || '',
image_url: item.track.album?.images?.[0]?.url || null,
},
duration_ms: item.track.duration_ms,
external_url: item.track.external_urls?.spotify || '',
},
}))
return {
success: true,
output: {
items,
next: data.next || null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Recently played tracks',
items: {
type: 'object',
properties: {
played_at: { type: 'string', description: 'When the track was played' },
track: { type: 'object', description: 'Track information' },
},
},
},
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,106 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetSavedAlbumsParams {
accessToken: string
limit?: number
offset?: number
market?: string
}
interface SpotifyGetSavedAlbumsResponse extends ToolResponse {
output: {
albums: Array<{
added_at: string
album: {
id: string
name: string
artists: Array<{ id: string; name: string }>
total_tracks: number
release_date: string
image_url: string | null
external_url: string
}
}>
total: number
next: string | null
}
}
export const spotifyGetSavedAlbumsTool: ToolConfig<
SpotifyGetSavedAlbumsParams,
SpotifyGetSavedAlbumsResponse
> = {
id: 'spotify_get_saved_albums',
name: 'Spotify Get Saved Albums',
description: "Get the user's saved albums.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of albums to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first album to return',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/me/albums?limit=${limit}&offset=${offset}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetSavedAlbumsResponse> => {
const data = await response.json()
return {
success: true,
output: {
albums: (data.items || []).map((item: any) => ({
added_at: item.added_at,
album: {
id: item.album.id,
name: item.album.name,
artists: item.album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
total_tracks: item.album.total_tracks,
release_date: item.album.release_date,
image_url: item.album.images?.[0]?.url || null,
external_url: item.album.external_urls?.spotify || '',
},
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
albums: { type: 'json', description: 'List of saved albums' },
total: { type: 'number', description: 'Total saved albums' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,97 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetSavedAudiobooksParams {
accessToken: string
limit?: number
offset?: number
}
interface SpotifyGetSavedAudiobooksResponse extends ToolResponse {
output: {
audiobooks: Array<{
added_at: string
audiobook: {
id: string
name: string
authors: Array<{ name: string }>
total_chapters: number
image_url: string | null
external_url: string
}
}>
total: number
next: string | null
}
}
export const spotifyGetSavedAudiobooksTool: ToolConfig<
SpotifyGetSavedAudiobooksParams,
SpotifyGetSavedAudiobooksResponse
> = {
id: 'spotify_get_saved_audiobooks',
name: 'Spotify Get Saved Audiobooks',
description: "Get the user's saved audiobooks.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of audiobooks to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first audiobook to return',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
return `https://api.spotify.com/v1/me/audiobooks?limit=${limit}&offset=${offset}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetSavedAudiobooksResponse> => {
const data = await response.json()
return {
success: true,
output: {
audiobooks: (data.items || []).map((item: any) => ({
added_at: item.added_at,
audiobook: {
id: item.audiobook?.id || item.id,
name: item.audiobook?.name || item.name,
authors: item.audiobook?.authors || item.authors || [],
total_chapters: item.audiobook?.total_chapters || item.total_chapters || 0,
image_url: item.audiobook?.images?.[0]?.url || item.images?.[0]?.url || null,
external_url:
item.audiobook?.external_urls?.spotify || item.external_urls?.spotify || '',
},
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
audiobooks: { type: 'json', description: 'List of saved audiobooks' },
total: { type: 'number', description: 'Total saved audiobooks' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,106 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetSavedEpisodesParams {
accessToken: string
limit?: number
offset?: number
market?: string
}
interface SpotifyGetSavedEpisodesResponse extends ToolResponse {
output: {
episodes: Array<{
added_at: string
episode: {
id: string
name: string
duration_ms: number
release_date: string
show: { id: string; name: string }
image_url: string | null
external_url: string
}
}>
total: number
next: string | null
}
}
export const spotifyGetSavedEpisodesTool: ToolConfig<
SpotifyGetSavedEpisodesParams,
SpotifyGetSavedEpisodesResponse
> = {
id: 'spotify_get_saved_episodes',
name: 'Spotify Get Saved Episodes',
description: "Get the user's saved podcast episodes.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read', 'user-read-playback-position'],
},
params: {
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of episodes to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first episode to return',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/me/episodes?limit=${limit}&offset=${offset}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetSavedEpisodesResponse> => {
const data = await response.json()
return {
success: true,
output: {
episodes: (data.items || []).map((item: any) => ({
added_at: item.added_at,
episode: {
id: item.episode.id,
name: item.episode.name,
duration_ms: item.episode.duration_ms || 0,
release_date: item.episode.release_date || '',
show: { id: item.episode.show?.id || '', name: item.episode.show?.name || '' },
image_url: item.episode.images?.[0]?.url || null,
external_url: item.episode.external_urls?.spotify || '',
},
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
episodes: { type: 'json', description: 'List of saved episodes' },
total: { type: 'number', description: 'Total saved episodes' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,96 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetSavedShowsParams {
accessToken: string
limit?: number
offset?: number
}
interface SpotifyGetSavedShowsResponse extends ToolResponse {
output: {
shows: Array<{
added_at: string
show: {
id: string
name: string
publisher: string
total_episodes: number
image_url: string | null
external_url: string
}
}>
total: number
next: string | null
}
}
export const spotifyGetSavedShowsTool: ToolConfig<
SpotifyGetSavedShowsParams,
SpotifyGetSavedShowsResponse
> = {
id: 'spotify_get_saved_shows',
name: 'Spotify Get Saved Shows',
description: "Get the user's saved podcast shows.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of shows to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first show to return',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
return `https://api.spotify.com/v1/me/shows?limit=${limit}&offset=${offset}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetSavedShowsResponse> => {
const data = await response.json()
return {
success: true,
output: {
shows: (data.items || []).map((item: any) => ({
added_at: item.added_at,
show: {
id: item.show.id,
name: item.show.name,
publisher: item.show.publisher || '',
total_episodes: item.show.total_episodes || 0,
image_url: item.show.images?.[0]?.url || null,
external_url: item.show.external_urls?.spotify || '',
},
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
shows: { type: 'json', description: 'List of saved shows' },
total: { type: 'number', description: 'Total saved shows' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,104 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetSavedTracksParams, SpotifyGetSavedTracksResponse } from './types'
export const spotifyGetSavedTracksTool: ToolConfig<
SpotifyGetSavedTracksParams,
SpotifyGetSavedTracksResponse
> = {
id: 'spotify_get_saved_tracks',
name: 'Spotify Get Saved Tracks',
description: "Get the current user's saved/liked tracks from their library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-read'],
},
params: {
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of tracks to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first track to return',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/me/tracks?limit=${limit}&offset=${offset}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetSavedTracksResponse> => {
const data = await response.json()
const tracks = (data.items || []).map((item: any) => ({
added_at: item.added_at,
track: {
id: item.track.id,
name: item.track.name,
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: item.track.album?.id || '',
name: item.track.album?.name || '',
image_url: item.track.album?.images?.[0]?.url || null,
},
duration_ms: item.track.duration_ms,
popularity: item.track.popularity,
external_url: item.track.external_urls?.spotify || '',
},
}))
return {
success: true,
output: {
tracks,
total: data.total || tracks.length,
next: data.next || null,
},
}
},
outputs: {
tracks: {
type: 'array',
description: "User's saved tracks",
items: {
type: 'object',
properties: {
added_at: { type: 'string', description: 'When the track was saved' },
track: { type: 'object', description: 'Track information' },
},
},
},
total: { type: 'number', description: 'Total number of saved tracks' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,89 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetShowParams {
accessToken: string
showId: string
market?: string
}
interface SpotifyGetShowResponse extends ToolResponse {
output: {
id: string
name: string
description: string
publisher: string
total_episodes: number
explicit: boolean
languages: string[]
image_url: string | null
external_url: string
}
}
export const spotifyGetShowTool: ToolConfig<SpotifyGetShowParams, SpotifyGetShowResponse> = {
id: 'spotify_get_show',
name: 'Spotify Get Show',
description: 'Get details for a podcast show.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
showId: {
type: 'string',
required: true,
description: 'The Spotify show ID',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/shows/${params.showId}`
if (params.market) url += `?market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetShowResponse> => {
const show = await response.json()
return {
success: true,
output: {
id: show.id,
name: show.name,
description: show.description || '',
publisher: show.publisher || '',
total_episodes: show.total_episodes || 0,
explicit: show.explicit || false,
languages: show.languages || [],
image_url: show.images?.[0]?.url || null,
external_url: show.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'Show ID' },
name: { type: 'string', description: 'Show name' },
description: { type: 'string', description: 'Show description' },
publisher: { type: 'string', description: 'Publisher name' },
total_episodes: { type: 'number', description: 'Total episodes' },
explicit: { type: 'boolean', description: 'Contains explicit content' },
languages: { type: 'json', description: 'Languages' },
image_url: { type: 'string', description: 'Cover image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,106 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetShowEpisodesParams {
accessToken: string
showId: string
limit?: number
offset?: number
market?: string
}
interface SpotifyGetShowEpisodesResponse extends ToolResponse {
output: {
episodes: Array<{
id: string
name: string
description: string
duration_ms: number
release_date: string
image_url: string | null
external_url: string
}>
total: number
next: string | null
}
}
export const spotifyGetShowEpisodesTool: ToolConfig<
SpotifyGetShowEpisodesParams,
SpotifyGetShowEpisodesResponse
> = {
id: 'spotify_get_show_episodes',
name: 'Spotify Get Show Episodes',
description: 'Get episodes from a podcast show.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
showId: {
type: 'string',
required: true,
description: 'The Spotify show ID',
},
limit: {
type: 'number',
required: false,
default: 20,
description: 'Number of episodes to return (1-50)',
},
offset: {
type: 'number',
required: false,
default: 0,
description: 'Index of first episode to return',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/shows/${params.showId}/episodes?limit=${limit}&offset=${offset}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetShowEpisodesResponse> => {
const data = await response.json()
return {
success: true,
output: {
episodes: (data.items || []).map((ep: any) => ({
id: ep.id,
name: ep.name,
description: ep.description || '',
duration_ms: ep.duration_ms || 0,
release_date: ep.release_date || '',
image_url: ep.images?.[0]?.url || null,
external_url: ep.external_urls?.spotify || '',
})),
total: data.total || 0,
next: data.next || null,
},
}
},
outputs: {
episodes: { type: 'json', description: 'List of episodes' },
total: { type: 'number', description: 'Total episodes' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,84 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetShowsParams {
accessToken: string
showIds: string
market?: string
}
interface SpotifyGetShowsResponse extends ToolResponse {
output: {
shows: Array<{
id: string
name: string
publisher: string
total_episodes: number
image_url: string | null
external_url: string
}>
}
}
export const spotifyGetShowsTool: ToolConfig<SpotifyGetShowsParams, SpotifyGetShowsResponse> = {
id: 'spotify_get_shows',
name: 'Spotify Get Multiple Shows',
description: 'Get details for multiple podcast shows.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-playback-position'],
},
params: {
showIds: {
type: 'string',
required: true,
description: 'Comma-separated show IDs (max 50)',
},
market: {
type: 'string',
required: false,
description: 'ISO country code for market',
},
},
request: {
url: (params) => {
const ids = params.showIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
let url = `https://api.spotify.com/v1/shows?ids=${ids}`
if (params.market) url += `&market=${params.market}`
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetShowsResponse> => {
const data = await response.json()
return {
success: true,
output: {
shows: (data.shows || []).map((show: any) => ({
id: show.id,
name: show.name,
publisher: show.publisher || '',
total_episodes: show.total_episodes || 0,
image_url: show.images?.[0]?.url || null,
external_url: show.external_urls?.spotify || '',
})),
},
}
},
outputs: {
shows: { type: 'json', description: 'List of shows' },
},
}

View File

@@ -0,0 +1,100 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetTopArtistsResponse, SpotifyGetTopItemsParams } from './types'
export const spotifyGetTopArtistsTool: ToolConfig<
SpotifyGetTopItemsParams,
SpotifyGetTopArtistsResponse
> = {
id: 'spotify_get_top_artists',
name: 'Spotify Get Top Artists',
description: "Get the current user's top artists based on listening history.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-top-read'],
},
params: {
time_range: {
type: 'string',
required: false,
visibility: 'user-or-llm',
default: 'medium_term',
description: 'Time range: short_term (~4 weeks), medium_term (~6 months), long_term (years)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of artists to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first artist to return',
},
},
request: {
url: (params) => {
const timeRange = params.time_range || 'medium_term'
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
return `https://api.spotify.com/v1/me/top/artists?time_range=${timeRange}&limit=${limit}&offset=${offset}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetTopArtistsResponse> => {
const data = await response.json()
const artists = (data.items || []).map((artist: any) => ({
id: artist.id,
name: artist.name,
genres: artist.genres || [],
popularity: artist.popularity,
followers: artist.followers?.total || 0,
image_url: artist.images?.[0]?.url || null,
external_url: artist.external_urls?.spotify || '',
}))
return {
success: true,
output: {
artists,
total: data.total || artists.length,
next: data.next || null,
},
}
},
outputs: {
artists: {
type: 'array',
description: "User's top artists",
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify artist ID' },
name: { type: 'string', description: 'Artist name' },
genres: { type: 'array', description: 'List of genres' },
popularity: { type: 'number', description: 'Popularity score' },
followers: { type: 'number', description: 'Number of followers' },
image_url: { type: 'string', description: 'Artist image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
total: { type: 'number', description: 'Total number of top artists' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,104 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetTopItemsParams, SpotifyGetTopTracksResponse } from './types'
export const spotifyGetTopTracksTool: ToolConfig<
SpotifyGetTopItemsParams,
SpotifyGetTopTracksResponse
> = {
id: 'spotify_get_top_tracks',
name: 'Spotify Get Top Tracks',
description: "Get the current user's top tracks based on listening history.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-top-read'],
},
params: {
time_range: {
type: 'string',
required: false,
visibility: 'user-or-llm',
default: 'medium_term',
description: 'Time range: short_term (~4 weeks), medium_term (~6 months), long_term (years)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Number of tracks to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first track to return',
},
},
request: {
url: (params) => {
const timeRange = params.time_range || 'medium_term'
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
return `https://api.spotify.com/v1/me/top/tracks?time_range=${timeRange}&limit=${limit}&offset=${offset}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetTopTracksResponse> => {
const data = await response.json()
const tracks = (data.items || []).map((track: any) => ({
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: track.album?.id || '',
name: track.album?.name || '',
image_url: track.album?.images?.[0]?.url || null,
},
duration_ms: track.duration_ms,
popularity: track.popularity,
external_url: track.external_urls?.spotify || '',
}))
return {
success: true,
output: {
tracks,
total: data.total || tracks.length,
next: data.next || null,
},
}
},
outputs: {
tracks: {
type: 'array',
description: "User's top tracks",
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify track ID' },
name: { type: 'string', description: 'Track name' },
artists: { type: 'array', description: 'List of artists' },
album: { type: 'object', description: 'Album information' },
duration_ms: { type: 'number', description: 'Duration in milliseconds' },
popularity: { type: 'number', description: 'Popularity score' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
total: { type: 'number', description: 'Total number of top tracks' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,81 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetTrackParams, SpotifyGetTrackResponse } from './types'
export const spotifyGetTrackTool: ToolConfig<SpotifyGetTrackParams, SpotifyGetTrackResponse> = {
id: 'spotify_get_track',
name: 'Spotify Get Track',
description: 'Get detailed information about a specific track on Spotify by its ID.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
trackId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the track',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code for track availability',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/tracks/${params.trackId}`
if (params.market) {
url += `?market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetTrackResponse> => {
const track = await response.json()
return {
success: true,
output: {
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: track.album?.id || '',
name: track.album?.name || '',
image_url: track.album?.images?.[0]?.url || null,
},
duration_ms: track.duration_ms,
explicit: track.explicit,
popularity: track.popularity,
preview_url: track.preview_url,
external_url: track.external_urls?.spotify || '',
uri: track.uri,
},
}
},
outputs: {
id: { type: 'string', description: 'Spotify track ID' },
name: { type: 'string', description: 'Track name' },
artists: { type: 'array', description: 'List of artists' },
album: { type: 'object', description: 'Album information' },
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
explicit: { type: 'boolean', description: 'Whether the track has explicit content' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
preview_url: { type: 'string', description: 'URL to 30-second preview', optional: true },
external_url: { type: 'string', description: 'Spotify URL' },
uri: { type: 'string', description: 'Spotify URI for the track' },
},
}

View File

@@ -0,0 +1,94 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetTracksParams, SpotifyGetTracksResponse } from './types'
export const spotifyGetTracksTool: ToolConfig<SpotifyGetTracksParams, SpotifyGetTracksResponse> = {
id: 'spotify_get_tracks',
name: 'Spotify Get Multiple Tracks',
description: 'Get detailed information about multiple tracks on Spotify by their IDs (up to 50).',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
trackIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of Spotify track IDs (max 50)',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code for track availability',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/tracks?ids=${encodeURIComponent(params.trackIds)}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetTracksResponse> => {
const data = await response.json()
const tracks = (data.tracks || [])
.filter((t: any) => t !== null)
.map((track: any) => ({
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
album: {
id: track.album?.id || '',
name: track.album?.name || '',
image_url: track.album?.images?.[0]?.url || null,
},
duration_ms: track.duration_ms,
explicit: track.explicit,
popularity: track.popularity,
preview_url: track.preview_url,
external_url: track.external_urls?.spotify || '',
}))
return {
success: true,
output: {
tracks,
},
}
},
outputs: {
tracks: {
type: 'array',
description: 'List of tracks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify track ID' },
name: { type: 'string', description: 'Track name' },
artists: { type: 'array', description: 'List of artists' },
album: { type: 'object', description: 'Album information' },
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
explicit: { type: 'boolean', description: 'Whether the track has explicit content' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
preview_url: { type: 'string', description: 'URL to 30-second preview' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
},
}

View File

@@ -0,0 +1,96 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyGetUserPlaylistsParams, SpotifyGetUserPlaylistsResponse } from './types'
export const spotifyGetUserPlaylistsTool: ToolConfig<
SpotifyGetUserPlaylistsParams,
SpotifyGetUserPlaylistsResponse
> = {
id: 'spotify_get_user_playlists',
name: 'Spotify Get User Playlists',
description: "Get the current user's playlists on Spotify.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-read-private', 'playlist-read-collaborative'],
},
params: {
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Maximum number of playlists to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first playlist to return',
},
},
request: {
url: (params) => {
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
return `https://api.spotify.com/v1/me/playlists?limit=${limit}&offset=${offset}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifyGetUserPlaylistsResponse> => {
const data = await response.json()
const playlists = (data.items || []).map((playlist: any) => ({
id: playlist.id,
name: playlist.name,
description: playlist.description,
public: playlist.public,
collaborative: playlist.collaborative,
owner: playlist.owner?.display_name || '',
total_tracks: playlist.tracks?.total || 0,
image_url: playlist.images?.[0]?.url || null,
external_url: playlist.external_urls?.spotify || '',
}))
return {
success: true,
output: {
playlists,
total: data.total || playlists.length,
next: data.next || null,
},
}
},
outputs: {
playlists: {
type: 'array',
description: "User's playlists",
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify playlist ID' },
name: { type: 'string', description: 'Playlist name' },
description: { type: 'string', description: 'Playlist description' },
public: { type: 'boolean', description: 'Whether public' },
collaborative: { type: 'boolean', description: 'Whether collaborative' },
owner: { type: 'string', description: 'Owner display name' },
total_tracks: { type: 'number', description: 'Number of tracks' },
image_url: { type: 'string', description: 'Cover image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
total: { type: 'number', description: 'Total number of playlists' },
next: { type: 'string', description: 'URL for next page', optional: true },
},
}

View File

@@ -0,0 +1,70 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyGetUserProfileParams {
accessToken: string
userId: string
}
interface SpotifyGetUserProfileResponse extends ToolResponse {
output: {
id: string
display_name: string | null
followers: number
image_url: string | null
external_url: string
}
}
export const spotifyGetUserProfileTool: ToolConfig<
SpotifyGetUserProfileParams,
SpotifyGetUserProfileResponse
> = {
id: 'spotify_get_user_profile',
name: 'Spotify Get User Profile',
description: "Get a user's public profile.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-read-private'],
},
params: {
userId: {
type: 'string',
required: true,
description: 'The Spotify user ID',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/users/${params.userId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response): Promise<SpotifyGetUserProfileResponse> => {
const user = await response.json()
return {
success: true,
output: {
id: user.id,
display_name: user.display_name || null,
followers: user.followers?.total || 0,
image_url: user.images?.[0]?.url || null,
external_url: user.external_urls?.spotify || '',
},
}
},
outputs: {
id: { type: 'string', description: 'User ID' },
display_name: { type: 'string', description: 'Display name' },
followers: { type: 'number', description: 'Number of followers' },
image_url: { type: 'string', description: 'Profile image URL' },
external_url: { type: 'string', description: 'Spotify URL' },
},
}

View File

@@ -0,0 +1,92 @@
// Search & Discovery
export { spotifyAddPlaylistCoverTool } from './add_playlist_cover'
// Player Controls
export { spotifyAddToQueueTool } from './add_to_queue'
export { spotifyAddTracksToPlaylistTool } from './add_tracks_to_playlist'
export { spotifyCheckFollowingTool } from './check_following'
export { spotifyCheckPlaylistFollowersTool } from './check_playlist_followers'
export { spotifyCheckSavedAlbumsTool } from './check_saved_albums'
export { spotifyCheckSavedAudiobooksTool } from './check_saved_audiobooks'
export { spotifyCheckSavedEpisodesTool } from './check_saved_episodes'
export { spotifyCheckSavedShowsTool } from './check_saved_shows'
export { spotifyCheckSavedTracksTool } from './check_saved_tracks'
export { spotifyCreatePlaylistTool } from './create_playlist'
export { spotifyFollowArtistsTool } from './follow_artists'
export { spotifyFollowPlaylistTool } from './follow_playlist'
// Albums
export { spotifyGetAlbumTool } from './get_album'
export { spotifyGetAlbumTracksTool } from './get_album_tracks'
export { spotifyGetAlbumsTool } from './get_albums'
// Artists
export { spotifyGetArtistTool } from './get_artist'
export { spotifyGetArtistAlbumsTool } from './get_artist_albums'
export { spotifyGetArtistTopTracksTool } from './get_artist_top_tracks'
export { spotifyGetArtistsTool } from './get_artists'
// Audiobooks
export { spotifyGetAudiobookTool } from './get_audiobook'
export { spotifyGetAudiobookChaptersTool } from './get_audiobook_chapters'
export { spotifyGetAudiobooksTool } from './get_audiobooks'
export { spotifyGetCategoriesTool } from './get_categories'
// User Profile & Library
export { spotifyGetCurrentUserTool } from './get_current_user'
export { spotifyGetCurrentlyPlayingTool } from './get_currently_playing'
export { spotifyGetDevicesTool } from './get_devices'
// Episodes
export { spotifyGetEpisodeTool } from './get_episode'
export { spotifyGetEpisodesTool } from './get_episodes'
export { spotifyGetFollowedArtistsTool } from './get_followed_artists'
export { spotifyGetMarketsTool } from './get_markets'
// Browse
export { spotifyGetNewReleasesTool } from './get_new_releases'
// Player Controls
export { spotifyGetPlaybackStateTool } from './get_playback_state'
// Playlists
export { spotifyGetPlaylistTool } from './get_playlist'
export { spotifyGetPlaylistCoverTool } from './get_playlist_cover'
export { spotifyGetPlaylistTracksTool } from './get_playlist_tracks'
export { spotifyGetQueueTool } from './get_queue'
export { spotifyGetRecentlyPlayedTool } from './get_recently_played'
export { spotifyGetSavedAlbumsTool } from './get_saved_albums'
export { spotifyGetSavedAudiobooksTool } from './get_saved_audiobooks'
export { spotifyGetSavedEpisodesTool } from './get_saved_episodes'
export { spotifyGetSavedShowsTool } from './get_saved_shows'
export { spotifyGetSavedTracksTool } from './get_saved_tracks'
// Shows (Podcasts)
export { spotifyGetShowTool } from './get_show'
export { spotifyGetShowEpisodesTool } from './get_show_episodes'
export { spotifyGetShowsTool } from './get_shows'
export { spotifyGetTopArtistsTool } from './get_top_artists'
export { spotifyGetTopTracksTool } from './get_top_tracks'
// Tracks
export { spotifyGetTrackTool } from './get_track'
export { spotifyGetTracksTool } from './get_tracks'
export { spotifyGetUserPlaylistsTool } from './get_user_playlists'
export { spotifyGetUserProfileTool } from './get_user_profile'
export { spotifyPauseTool } from './pause'
export { spotifyPlayTool } from './play'
// Library Management
export { spotifyRemoveSavedAlbumsTool } from './remove_saved_albums'
export { spotifyRemoveSavedAudiobooksTool } from './remove_saved_audiobooks'
export { spotifyRemoveSavedEpisodesTool } from './remove_saved_episodes'
export { spotifyRemoveSavedShowsTool } from './remove_saved_shows'
export { spotifyRemoveSavedTracksTool } from './remove_saved_tracks'
export { spotifyRemoveTracksFromPlaylistTool } from './remove_tracks_from_playlist'
export { spotifyReorderPlaylistItemsTool } from './reorder_playlist_items'
export { spotifyReplacePlaylistItemsTool } from './replace_playlist_items'
export { spotifySaveAlbumsTool } from './save_albums'
export { spotifySaveAudiobooksTool } from './save_audiobooks'
export { spotifySaveEpisodesTool } from './save_episodes'
export { spotifySaveShowsTool } from './save_shows'
export { spotifySaveTracksTool } from './save_tracks'
export { spotifySearchTool } from './search'
export { spotifySeekTool } from './seek'
export { spotifySetRepeatTool } from './set_repeat'
export { spotifySetShuffleTool } from './set_shuffle'
export { spotifySetVolumeTool } from './set_volume'
export { spotifySkipNextTool } from './skip_next'
export { spotifySkipPreviousTool } from './skip_previous'
export { spotifyTransferPlaybackTool } from './transfer_playback'
export { spotifyUnfollowArtistsTool } from './unfollow_artists'
export { spotifyUnfollowPlaylistTool } from './unfollow_playlist'
export { spotifyUpdatePlaylistTool } from './update_playlist'

View File

@@ -0,0 +1,52 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyPauseParams, SpotifyPauseResponse } from './types'
export const spotifyPauseTool: ToolConfig<SpotifyPauseParams, SpotifyPauseResponse> = {
id: 'spotify_pause',
name: 'Spotify Pause',
description: 'Pause playback on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID to pause. If not provided, pauses active device.',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player/pause'
if (params.device_id) {
url += `?device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifyPauseResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether playback was paused' },
},
}

View File

@@ -0,0 +1,94 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifyPlayParams, SpotifyPlayResponse } from './types'
export const spotifyPlayTool: ToolConfig<SpotifyPlayParams, SpotifyPlayResponse> = {
id: 'spotify_play',
name: 'Spotify Play',
description:
'Start or resume playback on Spotify. Can play specific tracks, albums, or playlists.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID to play on. If not provided, plays on active device.',
},
context_uri: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Spotify URI of album, artist, or playlist to play (e.g., "spotify:album:xxx")',
},
uris: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated track URIs to play (e.g., "spotify:track:xxx,spotify:track:yyy")',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Position in context to start playing (0-based index)',
},
position_ms: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Position in track to start from (in milliseconds)',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player/play'
if (params.device_id) {
url += `?device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: any = {}
if (params.context_uri) {
body.context_uri = params.context_uri
}
if (params.uris) {
body.uris = params.uris.split(',').map((uri) => uri.trim())
}
if (params.offset !== undefined) {
body.offset = { position: params.offset }
}
if (params.position_ms !== undefined) {
body.position_ms = params.position_ms
}
return Object.keys(body).length > 0 ? body : undefined
},
},
transformResponse: async (): Promise<SpotifyPlayResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether playback started successfully' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyRemoveSavedAlbumsParams {
accessToken: string
albumIds: string
}
interface SpotifyRemoveSavedAlbumsResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyRemoveSavedAlbumsTool: ToolConfig<
SpotifyRemoveSavedAlbumsParams,
SpotifyRemoveSavedAlbumsResponse
> = {
id: 'spotify_remove_saved_albums',
name: 'Spotify Remove Saved Albums',
description: "Remove albums from the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
albumIds: {
type: 'string',
required: true,
description: 'Comma-separated album IDs (max 20)',
},
},
request: {
url: () => 'https://api.spotify.com/v1/me/albums',
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
ids: params.albumIds
.split(',')
.map((id) => id.trim())
.slice(0, 20),
}),
},
transformResponse: async (): Promise<SpotifyRemoveSavedAlbumsResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether albums were removed' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyRemoveSavedAudiobooksParams {
accessToken: string
audiobookIds: string
}
interface SpotifyRemoveSavedAudiobooksResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyRemoveSavedAudiobooksTool: ToolConfig<
SpotifyRemoveSavedAudiobooksParams,
SpotifyRemoveSavedAudiobooksResponse
> = {
id: 'spotify_remove_saved_audiobooks',
name: 'Spotify Remove Saved Audiobooks',
description: "Remove audiobooks from the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
audiobookIds: {
type: 'string',
required: true,
description: 'Comma-separated audiobook IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.audiobookIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/audiobooks?ids=${ids}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifyRemoveSavedAudiobooksResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether audiobooks were removed' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyRemoveSavedEpisodesParams {
accessToken: string
episodeIds: string
}
interface SpotifyRemoveSavedEpisodesResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyRemoveSavedEpisodesTool: ToolConfig<
SpotifyRemoveSavedEpisodesParams,
SpotifyRemoveSavedEpisodesResponse
> = {
id: 'spotify_remove_saved_episodes',
name: 'Spotify Remove Saved Episodes',
description: "Remove podcast episodes from the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
episodeIds: {
type: 'string',
required: true,
description: 'Comma-separated episode IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.episodeIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/episodes?ids=${ids}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifyRemoveSavedEpisodesResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether episodes were removed' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyRemoveSavedShowsParams {
accessToken: string
showIds: string
}
interface SpotifyRemoveSavedShowsResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyRemoveSavedShowsTool: ToolConfig<
SpotifyRemoveSavedShowsParams,
SpotifyRemoveSavedShowsResponse
> = {
id: 'spotify_remove_saved_shows',
name: 'Spotify Remove Saved Shows',
description: "Remove podcast shows from the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
showIds: {
type: 'string',
required: true,
description: 'Comma-separated show IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.showIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/shows?ids=${ids}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifyRemoveSavedShowsResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether shows were removed' },
},
}

View File

@@ -0,0 +1,63 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyRemoveSavedTracksParams {
accessToken: string
trackIds: string
}
interface SpotifyRemoveSavedTracksResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifyRemoveSavedTracksTool: ToolConfig<
SpotifyRemoveSavedTracksParams,
SpotifyRemoveSavedTracksResponse
> = {
id: 'spotify_remove_saved_tracks',
name: 'Spotify Remove Saved Tracks',
description: "Remove tracks from the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
trackIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated track IDs to remove (max 50)',
},
},
request: {
url: () => 'https://api.spotify.com/v1/me/tracks',
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
ids: params.trackIds
.split(',')
.map((id) => id.trim())
.slice(0, 50),
}),
},
transformResponse: async (): Promise<SpotifyRemoveSavedTracksResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether tracks were removed successfully' },
},
}

View File

@@ -0,0 +1,65 @@
import type { ToolConfig } from '@/tools/types'
import type {
SpotifyRemoveTracksFromPlaylistParams,
SpotifyRemoveTracksFromPlaylistResponse,
} from './types'
export const spotifyRemoveTracksFromPlaylistTool: ToolConfig<
SpotifyRemoveTracksFromPlaylistParams,
SpotifyRemoveTracksFromPlaylistResponse
> = {
id: 'spotify_remove_tracks_from_playlist',
name: 'Spotify Remove Tracks from Playlist',
description: 'Remove one or more tracks from a Spotify playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Spotify ID of the playlist',
},
uris: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated Spotify URIs to remove (e.g., "spotify:track:xxx,spotify:track:yyy")',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const uris = params.uris.split(',').map((uri) => ({ uri: uri.trim() }))
return { tracks: uris }
},
},
transformResponse: async (response): Promise<SpotifyRemoveTracksFromPlaylistResponse> => {
const data = await response.json()
return {
success: true,
output: {
snapshot_id: data.snapshot_id,
},
}
},
outputs: {
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
},
}

View File

@@ -0,0 +1,91 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyReorderPlaylistItemsParams {
accessToken: string
playlistId: string
range_start: number
insert_before: number
range_length?: number
snapshot_id?: string
}
interface SpotifyReorderPlaylistItemsResponse extends ToolResponse {
output: {
snapshot_id: string
}
}
export const spotifyReorderPlaylistItemsTool: ToolConfig<
SpotifyReorderPlaylistItemsParams,
SpotifyReorderPlaylistItemsResponse
> = {
id: 'spotify_reorder_playlist_items',
name: 'Spotify Reorder Playlist Items',
description: 'Move tracks to a different position in a playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
range_start: {
type: 'number',
required: true,
description: 'Start index of items to reorder',
},
insert_before: {
type: 'number',
required: true,
description: 'Index to insert items before',
},
range_length: {
type: 'number',
required: false,
default: 1,
description: 'Number of items to reorder',
},
snapshot_id: {
type: 'string',
required: false,
description: 'Playlist snapshot ID for concurrency control',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {
range_start: params.range_start,
insert_before: params.insert_before,
range_length: params.range_length || 1,
}
if (params.snapshot_id) body.snapshot_id = params.snapshot_id
return body
},
},
transformResponse: async (response): Promise<SpotifyReorderPlaylistItemsResponse> => {
const data = await response.json()
return {
success: true,
output: { snapshot_id: data.snapshot_id || '' },
}
},
outputs: {
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
},
}

View File

@@ -0,0 +1,69 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyReplacePlaylistItemsParams {
accessToken: string
playlistId: string
uris: string
}
interface SpotifyReplacePlaylistItemsResponse extends ToolResponse {
output: {
snapshot_id: string
}
}
export const spotifyReplacePlaylistItemsTool: ToolConfig<
SpotifyReplacePlaylistItemsParams,
SpotifyReplacePlaylistItemsResponse
> = {
id: 'spotify_replace_playlist_items',
name: 'Spotify Replace Playlist Items',
description: 'Replace all items in a playlist with new tracks.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
uris: {
type: 'string',
required: true,
description: 'Comma-separated Spotify URIs (max 100)',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
uris: params.uris
.split(',')
.map((uri) => uri.trim())
.slice(0, 100),
}),
},
transformResponse: async (response): Promise<SpotifyReplacePlaylistItemsResponse> => {
const data = await response.json()
return {
success: true,
output: { snapshot_id: data.snapshot_id || '' },
}
},
outputs: {
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
},
}

View File

@@ -0,0 +1,55 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySaveAlbumsParams {
accessToken: string
albumIds: string
}
interface SpotifySaveAlbumsResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifySaveAlbumsTool: ToolConfig<SpotifySaveAlbumsParams, SpotifySaveAlbumsResponse> =
{
id: 'spotify_save_albums',
name: 'Spotify Save Albums',
description: "Save albums to the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
albumIds: {
type: 'string',
required: true,
description: 'Comma-separated album IDs (max 20)',
},
},
request: {
url: () => 'https://api.spotify.com/v1/me/albums',
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
ids: params.albumIds
.split(',')
.map((id) => id.trim())
.slice(0, 20),
}),
},
transformResponse: async (): Promise<SpotifySaveAlbumsResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether albums were saved' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySaveAudiobooksParams {
accessToken: string
audiobookIds: string
}
interface SpotifySaveAudiobooksResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifySaveAudiobooksTool: ToolConfig<
SpotifySaveAudiobooksParams,
SpotifySaveAudiobooksResponse
> = {
id: 'spotify_save_audiobooks',
name: 'Spotify Save Audiobooks',
description: "Save audiobooks to the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
audiobookIds: {
type: 'string',
required: true,
description: 'Comma-separated audiobook IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.audiobookIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/audiobooks?ids=${ids}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifySaveAudiobooksResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether audiobooks were saved' },
},
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySaveEpisodesParams {
accessToken: string
episodeIds: string
}
interface SpotifySaveEpisodesResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifySaveEpisodesTool: ToolConfig<
SpotifySaveEpisodesParams,
SpotifySaveEpisodesResponse
> = {
id: 'spotify_save_episodes',
name: 'Spotify Save Episodes',
description: "Save podcast episodes to the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
episodeIds: {
type: 'string',
required: true,
description: 'Comma-separated episode IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.episodeIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/episodes?ids=${ids}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifySaveEpisodesResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether episodes were saved' },
},
}

View File

@@ -0,0 +1,54 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySaveShowsParams {
accessToken: string
showIds: string
}
interface SpotifySaveShowsResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifySaveShowsTool: ToolConfig<SpotifySaveShowsParams, SpotifySaveShowsResponse> = {
id: 'spotify_save_shows',
name: 'Spotify Save Shows',
description: "Save podcast shows to the user's library.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
showIds: {
type: 'string',
required: true,
description: 'Comma-separated show IDs (max 50)',
},
},
request: {
url: (params) => {
const ids = params.showIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/shows?ids=${ids}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifySaveShowsResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether shows were saved' },
},
}

View File

@@ -0,0 +1,48 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifySaveTracksParams, SpotifySaveTracksResponse } from './types'
export const spotifySaveTracksTool: ToolConfig<SpotifySaveTracksParams, SpotifySaveTracksResponse> =
{
id: 'spotify_save_tracks',
name: 'Spotify Save Tracks',
description: "Save tracks to the current user's library (like tracks).",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-library-modify'],
},
params: {
trackIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated Spotify track IDs to save (max 50)',
},
},
request: {
url: (params) =>
`https://api.spotify.com/v1/me/tracks?ids=${encodeURIComponent(params.trackIds)}`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySaveTracksResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the tracks were saved successfully' },
},
}

View File

@@ -0,0 +1,157 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifySearchParams, SpotifySearchResponse } from './types'
export const spotifySearchTool: ToolConfig<SpotifySearchParams, SpotifySearchResponse> = {
id: 'spotify_search',
name: 'Spotify Search',
description:
'Search for tracks, albums, artists, or playlists on Spotify. Returns matching results based on the query.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
},
params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search query (e.g., "Bohemian Rhapsody", "artist:Queen", "genre:rock")',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
default: 'track',
description:
'Type of results: track, album, artist, playlist, or comma-separated (e.g., "track,artist")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
default: 20,
description: 'Maximum number of results to return (1-50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-only',
default: 0,
description: 'Index of the first result to return for pagination',
},
market: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ISO 3166-1 alpha-2 country code to filter results (e.g., "US", "GB")',
},
},
request: {
url: (params) => {
const type = params.type || 'track'
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
const offset = params.offset || 0
let url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(params.query)}&type=${encodeURIComponent(type)}&limit=${limit}&offset=${offset}`
if (params.market) {
url += `&market=${params.market}`
}
return url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response): Promise<SpotifySearchResponse> => {
const data = await response.json()
const tracks = (data.tracks?.items || []).map((track: any) => ({
id: track.id,
name: track.name,
artists: track.artists?.map((a: any) => a.name) || [],
album: track.album?.name || '',
duration_ms: track.duration_ms,
popularity: track.popularity,
preview_url: track.preview_url,
external_url: track.external_urls?.spotify || '',
}))
const artists = (data.artists?.items || []).map((artist: any) => ({
id: artist.id,
name: artist.name,
genres: artist.genres || [],
popularity: artist.popularity,
followers: artist.followers?.total || 0,
image_url: artist.images?.[0]?.url || null,
external_url: artist.external_urls?.spotify || '',
}))
const albums = (data.albums?.items || []).map((album: any) => ({
id: album.id,
name: album.name,
artists: album.artists?.map((a: any) => a.name) || [],
total_tracks: album.total_tracks,
release_date: album.release_date,
image_url: album.images?.[0]?.url || null,
external_url: album.external_urls?.spotify || '',
}))
const playlists = (data.playlists?.items || []).map((playlist: any) => ({
id: playlist.id,
name: playlist.name,
description: playlist.description,
owner: playlist.owner?.display_name || '',
total_tracks: playlist.tracks?.total || 0,
image_url: playlist.images?.[0]?.url || null,
external_url: playlist.external_urls?.spotify || '',
}))
return {
success: true,
output: {
tracks,
artists,
albums,
playlists,
},
}
},
outputs: {
tracks: {
type: 'array',
description: 'List of matching tracks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Spotify track ID' },
name: { type: 'string', description: 'Track name' },
artists: { type: 'array', description: 'List of artist names' },
album: { type: 'string', description: 'Album name' },
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
popularity: { type: 'number', description: 'Popularity score (0-100)' },
preview_url: { type: 'string', description: 'URL to 30-second preview' },
external_url: { type: 'string', description: 'Spotify URL' },
},
},
},
artists: {
type: 'array',
description: 'List of matching artists',
},
albums: {
type: 'array',
description: 'List of matching albums',
},
playlists: {
type: 'array',
description: 'List of matching playlists',
},
},
}

View File

@@ -0,0 +1,67 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySeekParams {
accessToken: string
position_ms: number
device_id?: string
}
interface SpotifySeekResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifySeekTool: ToolConfig<SpotifySeekParams, SpotifySeekResponse> = {
id: 'spotify_seek',
name: 'Spotify Seek',
description: 'Seek to a position in the currently playing track.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
position_ms: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Position in milliseconds to seek to',
},
device_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Device ID to target',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/me/player/seek?position_ms=${params.position_ms}`
if (params.device_id) {
url += `&device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySeekResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether seek was successful' },
},
}

View File

@@ -0,0 +1,67 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySetRepeatParams {
accessToken: string
state: string
device_id?: string
}
interface SpotifySetRepeatResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifySetRepeatTool: ToolConfig<SpotifySetRepeatParams, SpotifySetRepeatResponse> = {
id: 'spotify_set_repeat',
name: 'Spotify Set Repeat',
description: 'Set the repeat mode for playback.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
state: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Repeat mode: "off", "track", or "context"',
},
device_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Device ID to target',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/me/player/repeat?state=${params.state}`
if (params.device_id) {
url += `&device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySetRepeatResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether repeat mode was set successfully' },
},
}

View File

@@ -0,0 +1,68 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifySetShuffleParams {
accessToken: string
state: boolean
device_id?: string
}
interface SpotifySetShuffleResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifySetShuffleTool: ToolConfig<SpotifySetShuffleParams, SpotifySetShuffleResponse> =
{
id: 'spotify_set_shuffle',
name: 'Spotify Set Shuffle',
description: 'Turn shuffle on or off.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
state: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'true for shuffle on, false for off',
},
device_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Device ID to target',
},
},
request: {
url: (params) => {
let url = `https://api.spotify.com/v1/me/player/shuffle?state=${params.state}`
if (params.device_id) {
url += `&device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySetShuffleResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether shuffle was set successfully' },
},
}

View File

@@ -0,0 +1,59 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifySetVolumeParams, SpotifySetVolumeResponse } from './types'
export const spotifySetVolumeTool: ToolConfig<SpotifySetVolumeParams, SpotifySetVolumeResponse> = {
id: 'spotify_set_volume',
name: 'Spotify Set Volume',
description: 'Set the playback volume on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
volume_percent: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Volume level (0 to 100)',
},
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID. If not provided, uses active device.',
},
},
request: {
url: (params) => {
const volume = Math.min(Math.max(params.volume_percent, 0), 100)
let url = `https://api.spotify.com/v1/me/player/volume?volume_percent=${volume}`
if (params.device_id) {
url += `&device_id=${params.device_id}`
}
return url
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySetVolumeResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether volume was set' },
},
}

View File

@@ -0,0 +1,52 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifySkipNextParams, SpotifySkipNextResponse } from './types'
export const spotifySkipNextTool: ToolConfig<SpotifySkipNextParams, SpotifySkipNextResponse> = {
id: 'spotify_skip_next',
name: 'Spotify Skip to Next',
description: 'Skip to the next track on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID. If not provided, uses active device.',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player/next'
if (params.device_id) {
url += `?device_id=${params.device_id}`
}
return url
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySkipNextResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether skip was successful' },
},
}

View File

@@ -0,0 +1,55 @@
import type { ToolConfig } from '@/tools/types'
import type { SpotifySkipPreviousParams, SpotifySkipPreviousResponse } from './types'
export const spotifySkipPreviousTool: ToolConfig<
SpotifySkipPreviousParams,
SpotifySkipPreviousResponse
> = {
id: 'spotify_skip_previous',
name: 'Spotify Skip to Previous',
description: 'Skip to the previous track on Spotify.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
device_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Device ID. If not provided, uses active device.',
},
},
request: {
url: (params) => {
let url = 'https://api.spotify.com/v1/me/player/previous'
if (params.device_id) {
url += `?device_id=${params.device_id}`
}
return url
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifySkipPreviousResponse> => {
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether skip was successful' },
},
}

View File

@@ -0,0 +1,69 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyTransferPlaybackParams {
accessToken: string
device_id: string
play?: boolean
}
interface SpotifyTransferPlaybackResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifyTransferPlaybackTool: ToolConfig<
SpotifyTransferPlaybackParams,
SpotifyTransferPlaybackResponse
> = {
id: 'spotify_transfer_playback',
name: 'Spotify Transfer Playback',
description: 'Transfer playback to a different device.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-modify-playback-state'],
},
params: {
device_id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID to transfer playback to',
},
play: {
type: 'boolean',
required: false,
visibility: 'user-only',
default: true,
description: 'Whether to start playing on the new device',
},
},
request: {
url: () => 'https://api.spotify.com/v1/me/player',
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
device_ids: [params.device_id],
play: params.play ?? true,
}),
},
transformResponse: async (): Promise<SpotifyTransferPlaybackResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether transfer was successful' },
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyUnfollowArtistsParams {
accessToken: string
artistIds: string
}
interface SpotifyUnfollowArtistsResponse extends ToolResponse {
output: {
success: boolean
}
}
export const spotifyUnfollowArtistsTool: ToolConfig<
SpotifyUnfollowArtistsParams,
SpotifyUnfollowArtistsResponse
> = {
id: 'spotify_unfollow_artists',
name: 'Spotify Unfollow Artists',
description: 'Unfollow one or more artists.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['user-follow-modify'],
},
params: {
artistIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated artist IDs to unfollow (max 50)',
},
},
request: {
url: (params) => {
const ids = params.artistIds
.split(',')
.map((id) => id.trim())
.slice(0, 50)
.join(',')
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (): Promise<SpotifyUnfollowArtistsResponse> => {
return {
success: true,
output: { success: true },
}
},
outputs: {
success: { type: 'boolean', description: 'Whether artists were unfollowed successfully' },
},
}

View File

@@ -0,0 +1,50 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyUnfollowPlaylistParams {
accessToken: string
playlistId: string
}
interface SpotifyUnfollowPlaylistResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyUnfollowPlaylistTool: ToolConfig<
SpotifyUnfollowPlaylistParams,
SpotifyUnfollowPlaylistResponse
> = {
id: 'spotify_unfollow_playlist',
name: 'Spotify Unfollow Playlist',
description: 'Unfollow (unsave) a playlist.',
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/followers`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (): Promise<SpotifyUnfollowPlaylistResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether unfollow succeeded' },
},
}

View File

@@ -0,0 +1,76 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface SpotifyUpdatePlaylistParams {
accessToken: string
playlistId: string
name?: string
description?: string
public?: boolean
}
interface SpotifyUpdatePlaylistResponse extends ToolResponse {
output: { success: boolean }
}
export const spotifyUpdatePlaylistTool: ToolConfig<
SpotifyUpdatePlaylistParams,
SpotifyUpdatePlaylistResponse
> = {
id: 'spotify_update_playlist',
name: 'Spotify Update Playlist',
description: "Update a playlist's name, description, or visibility.",
version: '1.0.0',
oauth: {
required: true,
provider: 'spotify',
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
},
params: {
playlistId: {
type: 'string',
required: true,
description: 'The Spotify playlist ID',
},
name: {
type: 'string',
required: false,
description: 'New name for the playlist',
},
description: {
type: 'string',
required: false,
description: 'New description for the playlist',
},
public: {
type: 'boolean',
required: false,
description: 'Whether the playlist should be public',
},
},
request: {
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.name !== undefined) body.name = params.name
if (params.description !== undefined) body.description = params.description
if (params.public !== undefined) body.public = params.public
return body
},
},
transformResponse: async (): Promise<SpotifyUpdatePlaylistResponse> => {
return { success: true, output: { success: true } }
},
outputs: {
success: { type: 'boolean', description: 'Whether update succeeded' },
},
}