@unclick/lastfm-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Apache License 2.0
2
+
3
+ Copyright (c) 2026 UnClick / malamutemayhem
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Last.fm MCP by UnClick
2
+
3
+ Music charts, artist info, similar artists, and top tracks from Last.fm.
4
+
5
+ > By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
6
+
7
+ ## Install
8
+
9
+ Installs straight from GitHub, no npm account needed.
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "lastfm": {
15
+ "command": "npx",
16
+ "args": ["-y", "https://github.com/malamutemayhem/unclick/releases/download/standalone-mcps-latest/lastfm.tgz"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## Tools
23
+
24
+ - `lastfm_artist_info`
25
+ - `lastfm_search_artists`
26
+ - `lastfm_top_tracks`
27
+ - `lastfm_similar_artists`
28
+ - `lastfm_chart_top_artists`
29
+ - `lastfm_chart_top_tracks`
30
+ - `lastfm_album_info`
31
+
32
+ ## Want the rest?
33
+
34
+ This is one connector. [UnClick](https://unclick.world) bundles 180+ tools plus
35
+ persistent cross-session agent memory in a single install.
36
+
37
+ ## License
38
+
39
+ Apache-2.0
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // Last.fm MCP. Standalone MCP server by UnClick.
3
+ // By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
4
+ //
5
+ // Generated from the UnClick connector by scripts/generate-standalone-mcp.mjs.
6
+ // Edit the connector in the UnClick monorepo, not here.
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
10
+ import { lastfmGetArtistInfo, lastfmSearchArtists, lastfmGetTopTracks, lastfmGetSimilarArtists, lastfmGetChartTopArtists, lastfmGetChartTopTracks, lastfmGetAlbumInfo, } from "./lastfm-tool.js";
11
+ const TOOLS = [
12
+ {
13
+ name: "lastfm_artist_info",
14
+ description: "Get Last.fm artist info.",
15
+ inputSchema: {
16
+ type: "object",
17
+ additionalProperties: false,
18
+ properties: {
19
+ artist: { type: "string" },
20
+ api_key: { type: "string" },
21
+ },
22
+ required: ["artist"],
23
+ },
24
+ },
25
+ {
26
+ name: "lastfm_search_artists",
27
+ description: "Search for artists on Last.fm.",
28
+ inputSchema: {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ properties: {
32
+ artist: { type: "string" },
33
+ limit: { type: "number" },
34
+ api_key: { type: "string" },
35
+ },
36
+ required: ["artist"],
37
+ },
38
+ },
39
+ {
40
+ name: "lastfm_top_tracks",
41
+ description: "Get top tracks for an artist on Last.fm.",
42
+ inputSchema: {
43
+ type: "object",
44
+ additionalProperties: false,
45
+ properties: {
46
+ artist: { type: "string" },
47
+ limit: { type: "number" },
48
+ api_key: { type: "string" },
49
+ },
50
+ required: ["artist"],
51
+ },
52
+ },
53
+ {
54
+ name: "lastfm_similar_artists",
55
+ description: "Get similar artists on Last.fm.",
56
+ inputSchema: {
57
+ type: "object",
58
+ additionalProperties: false,
59
+ properties: {
60
+ artist: { type: "string" },
61
+ limit: { type: "number" },
62
+ api_key: { type: "string" },
63
+ },
64
+ required: ["artist"],
65
+ },
66
+ },
67
+ {
68
+ name: "lastfm_chart_top_artists",
69
+ description: "Get the Last.fm chart of top artists.",
70
+ inputSchema: {
71
+ type: "object",
72
+ additionalProperties: false,
73
+ properties: {
74
+ limit: { type: "number" },
75
+ api_key: { type: "string" },
76
+ },
77
+ },
78
+ },
79
+ {
80
+ name: "lastfm_chart_top_tracks",
81
+ description: "Get the Last.fm chart of top tracks.",
82
+ inputSchema: {
83
+ type: "object",
84
+ additionalProperties: false,
85
+ properties: {
86
+ limit: { type: "number" },
87
+ api_key: { type: "string" },
88
+ },
89
+ },
90
+ },
91
+ {
92
+ name: "lastfm_album_info",
93
+ description: "Get album info from Last.fm.",
94
+ inputSchema: {
95
+ type: "object",
96
+ additionalProperties: false,
97
+ properties: {
98
+ artist: { type: "string" },
99
+ album: { type: "string" },
100
+ api_key: { type: "string" },
101
+ },
102
+ required: ["artist", "album"],
103
+ },
104
+ }
105
+ ];
106
+ const HANDLERS = {
107
+ lastfm_artist_info: (args) => lastfmGetArtistInfo(args),
108
+ lastfm_search_artists: (args) => lastfmSearchArtists(args),
109
+ lastfm_top_tracks: (args) => lastfmGetTopTracks(args),
110
+ lastfm_similar_artists: (args) => lastfmGetSimilarArtists(args),
111
+ lastfm_chart_top_artists: (args) => lastfmGetChartTopArtists(args),
112
+ lastfm_chart_top_tracks: (args) => lastfmGetChartTopTracks(args),
113
+ lastfm_album_info: (args) => lastfmGetAlbumInfo(args),
114
+ };
115
+ const server = new Server({ name: "io.github.malamutemayhem/lastfm", version: "0.1.0" }, { capabilities: { tools: {} } });
116
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
117
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
118
+ const handler = HANDLERS[req.params.name];
119
+ if (!handler) {
120
+ return { content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], isError: true };
121
+ }
122
+ try {
123
+ const result = await handler((req.params.arguments ?? {}));
124
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
125
+ }
126
+ catch (err) {
127
+ const message = err instanceof Error ? err.message : String(err);
128
+ return { content: [{ type: "text", text: message }], isError: true };
129
+ }
130
+ });
131
+ async function main() {
132
+ const transport = new StdioServerTransport();
133
+ await server.connect(transport);
134
+ }
135
+ main().catch((err) => {
136
+ process.stderr.write(`[lastfm-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
137
+ process.exit(1);
138
+ });
@@ -0,0 +1,168 @@
1
+ // ── Last.fm API tool ────────────────────────────────────────────────────────────
2
+ // Unlimited free read access. No OAuth needed for read operations.
3
+ // Docs: https://www.last.fm/api
4
+ // Env var: LASTFM_API_KEY
5
+ const LASTFM_BASE = "https://ws.audioscrobbler.com/2.0";
6
+ async function lastfmGet(apiKey, method, params) {
7
+ const qs = new URLSearchParams({
8
+ method,
9
+ api_key: apiKey,
10
+ format: "json",
11
+ });
12
+ for (const [k, v] of Object.entries(params)) {
13
+ if (v !== undefined && v !== "")
14
+ qs.set(k, String(v));
15
+ }
16
+ const res = await fetch(`${LASTFM_BASE}/?${qs}`);
17
+ if (!res.ok)
18
+ throw new Error(`Last.fm API HTTP ${res.status}: ${res.statusText}`);
19
+ const json = await res.json();
20
+ if (json.error)
21
+ throw new Error(`Last.fm error ${json.error}: ${json.message}`);
22
+ return json;
23
+ }
24
+ function getApiKey(args) {
25
+ const key = String(args.api_key ?? process.env.LASTFM_API_KEY ?? "").trim();
26
+ if (!key)
27
+ throw new Error("api_key is required (or set LASTFM_API_KEY env var).");
28
+ return key;
29
+ }
30
+ // ── Tool functions ─────────────────────────────────────────────────────────────
31
+ export async function lastfmGetArtistInfo(args) {
32
+ try {
33
+ const apiKey = getApiKey(args);
34
+ const artist = String(args.artist ?? "").trim();
35
+ if (!artist)
36
+ return { error: "artist is required." };
37
+ const params = { artist };
38
+ if (args.lang)
39
+ params.lang = String(args.lang);
40
+ const data = await lastfmGet(apiKey, "artist.getinfo", params);
41
+ return data.artist ?? data;
42
+ }
43
+ catch (err) {
44
+ return { error: err instanceof Error ? err.message : String(err) };
45
+ }
46
+ }
47
+ export async function lastfmSearchArtists(args) {
48
+ try {
49
+ const apiKey = getApiKey(args);
50
+ const query = String(args.query ?? "").trim();
51
+ if (!query)
52
+ return { error: "query is required." };
53
+ const params = { artist: query };
54
+ if (args.limit)
55
+ params.limit = Number(args.limit);
56
+ if (args.page)
57
+ params.page = Number(args.page);
58
+ const data = await lastfmGet(apiKey, "artist.search", params);
59
+ const results = data.results;
60
+ const matches = results?.artistmatches?.artist;
61
+ return {
62
+ total: results?.["opensearch:totalResults"],
63
+ artists: Array.isArray(matches) ? matches : matches ? [matches] : [],
64
+ };
65
+ }
66
+ catch (err) {
67
+ return { error: err instanceof Error ? err.message : String(err) };
68
+ }
69
+ }
70
+ export async function lastfmGetTopTracks(args) {
71
+ try {
72
+ const apiKey = getApiKey(args);
73
+ const artist = String(args.artist ?? "").trim();
74
+ if (!artist)
75
+ return { error: "artist is required." };
76
+ const params = { artist };
77
+ if (args.limit)
78
+ params.limit = Number(args.limit);
79
+ if (args.page)
80
+ params.page = Number(args.page);
81
+ const data = await lastfmGet(apiKey, "artist.gettoptracks", params);
82
+ const toptracks = data.toptracks;
83
+ return {
84
+ artist,
85
+ tracks: toptracks?.track ?? [],
86
+ };
87
+ }
88
+ catch (err) {
89
+ return { error: err instanceof Error ? err.message : String(err) };
90
+ }
91
+ }
92
+ export async function lastfmGetSimilarArtists(args) {
93
+ try {
94
+ const apiKey = getApiKey(args);
95
+ const artist = String(args.artist ?? "").trim();
96
+ if (!artist)
97
+ return { error: "artist is required." };
98
+ const params = { artist };
99
+ if (args.limit)
100
+ params.limit = Number(args.limit);
101
+ const data = await lastfmGet(apiKey, "artist.getsimilar", params);
102
+ const similar = data.similarartists;
103
+ return {
104
+ artist,
105
+ similar_artists: similar?.artist ?? [],
106
+ };
107
+ }
108
+ catch (err) {
109
+ return { error: err instanceof Error ? err.message : String(err) };
110
+ }
111
+ }
112
+ export async function lastfmGetChartTopArtists(args) {
113
+ try {
114
+ const apiKey = getApiKey(args);
115
+ const params = {};
116
+ if (args.limit)
117
+ params.limit = Number(args.limit);
118
+ if (args.page)
119
+ params.page = Number(args.page);
120
+ const data = await lastfmGet(apiKey, "chart.gettopartists", params);
121
+ const artists = data.artists;
122
+ return {
123
+ artists: artists?.artist ?? [],
124
+ total: artists?.["@attr"]?.total,
125
+ };
126
+ }
127
+ catch (err) {
128
+ return { error: err instanceof Error ? err.message : String(err) };
129
+ }
130
+ }
131
+ export async function lastfmGetChartTopTracks(args) {
132
+ try {
133
+ const apiKey = getApiKey(args);
134
+ const params = {};
135
+ if (args.limit)
136
+ params.limit = Number(args.limit);
137
+ if (args.page)
138
+ params.page = Number(args.page);
139
+ const data = await lastfmGet(apiKey, "chart.gettoptracks", params);
140
+ const tracks = data.tracks;
141
+ return {
142
+ tracks: tracks?.track ?? [],
143
+ total: tracks?.["@attr"]?.total,
144
+ };
145
+ }
146
+ catch (err) {
147
+ return { error: err instanceof Error ? err.message : String(err) };
148
+ }
149
+ }
150
+ export async function lastfmGetAlbumInfo(args) {
151
+ try {
152
+ const apiKey = getApiKey(args);
153
+ const artist = String(args.artist ?? "").trim();
154
+ const album = String(args.album ?? "").trim();
155
+ if (!artist)
156
+ return { error: "artist is required." };
157
+ if (!album)
158
+ return { error: "album is required." };
159
+ const params = { artist, album };
160
+ if (args.lang)
161
+ params.lang = String(args.lang);
162
+ const data = await lastfmGet(apiKey, "album.getinfo", params);
163
+ return data.album ?? data;
164
+ }
165
+ catch (err) {
166
+ return { error: err instanceof Error ? err.message : String(err) };
167
+ }
168
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@unclick/lastfm-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.malamutemayhem/lastfm",
5
+ "description": "Music charts, artist info, similar artists, and top tracks from Last.fm. By UnClick (https://unclick.world).",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "unclick",
10
+ "lastfm",
11
+ "music",
12
+ "charts"
13
+ ],
14
+ "author": "UnClick (https://unclick.world)",
15
+ "type": "module",
16
+ "bin": {
17
+ "lastfm-mcp": "./dist/index.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "server.json"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/index.js",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.15.1"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.6.0",
35
+ "@types/node": "^22.0.0"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/malamutemayhem/unclick.git",
41
+ "directory": "packages/standalone/lastfm-mcp"
42
+ },
43
+ "homepage": "https://unclick.world"
44
+ }
package/server.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.malamutemayhem/lastfm",
4
+ "title": "Last.fm MCP by UnClick",
5
+ "description": "Music charts, artist info, similar artists, and top tracks from Last.fm. By UnClick.",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://unclick.world",
8
+ "icons": [
9
+ {
10
+ "src": "https://unclick.world/favicon.png",
11
+ "mimeType": "image/png",
12
+ "sizes": [
13
+ "512x512"
14
+ ]
15
+ }
16
+ ],
17
+ "repository": {
18
+ "url": "https://github.com/malamutemayhem/unclick.git",
19
+ "source": "github",
20
+ "subfolder": "packages/standalone/lastfm-mcp"
21
+ },
22
+ "packages": [
23
+ {
24
+ "registryType": "npm",
25
+ "identifier": "@unclick/lastfm-mcp",
26
+ "version": "0.1.0",
27
+ "runtimeHint": "npx",
28
+ "transport": {
29
+ "type": "stdio"
30
+ }
31
+ }
32
+ ]
33
+ }