mcp-lyricscom 1.2.0 → 1.4.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/README.md +17 -0
- package/dist/index.js +174 -93
- package/dist/index.js.map +1 -1
- package/dist/lyricscom/client.js +1 -1
- package/dist/lyricscom/client.js.map +1 -1
- package/package.json +2 -2
- package/server.json +5 -6
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://github.com/smeet666/mcp-lyricscom/actions/workflows/ci.yml)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-lyricscom)
|
|
7
|
+
[](https://glama.ai/mcp/servers/smeet666/mcp-lyricscom)
|
|
7
8
|
|
|
8
9
|
An [MCP](https://modelcontextprotocol.io) server for [lyrics.com](https://www.lyrics.com).
|
|
9
10
|
Search songs by a word in their lyrics, by title, and read the full text.
|
|
@@ -41,6 +42,14 @@ claude mcp add lyricscom -- npx -y mcp-lyricscom
|
|
|
41
42
|
|
|
42
43
|
That is the whole setup. There is nothing to sign up for.
|
|
43
44
|
|
|
45
|
+
**Bundle, without npm**
|
|
46
|
+
|
|
47
|
+
Download `mcp-lyricscom-<version>.mcpb` from
|
|
48
|
+
[the latest release](https://github.com/smeet666/mcp-lyricscom/releases/latest) and open
|
|
49
|
+
it. A client that supports MCP bundles installs it on its own, with no npm and
|
|
50
|
+
no configuration file to edit. The bundle carries its dependencies, so nothing
|
|
51
|
+
is fetched at install time.
|
|
52
|
+
|
|
44
53
|
## Tools
|
|
45
54
|
|
|
46
55
|
| Tool | What it does | Key parameters |
|
|
@@ -240,6 +249,14 @@ claude mcp add lyricscom -- npx -y mcp-lyricscom
|
|
|
240
249
|
|
|
241
250
|
C'est toute l'installation. Il n'y a aucune inscription.
|
|
242
251
|
|
|
252
|
+
**Bundle, sans npm**
|
|
253
|
+
|
|
254
|
+
Téléchargez `mcp-lyricscom-<version>.mcpb` depuis
|
|
255
|
+
[la dernière release](https://github.com/smeet666/mcp-lyricscom/releases/latest) et
|
|
256
|
+
ouvrez-le. Un client compatible avec les bundles MCP l'installe seul, sans npm
|
|
257
|
+
ni fichier de configuration à modifier. Le bundle embarque ses dépendances,
|
|
258
|
+
donc rien n'est téléchargé à l'installation.
|
|
259
|
+
|
|
243
260
|
## Outils
|
|
244
261
|
|
|
245
262
|
| Outil | Rôle | Paramètres principaux |
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var PKG_VERSION = "1.
|
|
7
|
+
var PKG_VERSION = "1.4.0";
|
|
8
8
|
var REPO_URL = "https://github.com/smeet666/mcp-lyricscom";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
@@ -108,7 +108,7 @@ function createLogger(level) {
|
|
|
108
108
|
|
|
109
109
|
// src/server.ts
|
|
110
110
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
111
|
-
import { z as
|
|
111
|
+
import { z as z6 } from "zod";
|
|
112
112
|
|
|
113
113
|
// src/lyricscom/cache.ts
|
|
114
114
|
var TtlLruCache = class {
|
|
@@ -679,7 +679,7 @@ var LyricsComClient = class {
|
|
|
679
679
|
};
|
|
680
680
|
|
|
681
681
|
// src/tools/getLyrics.ts
|
|
682
|
-
import { z as
|
|
682
|
+
import { z as z3 } from "zod";
|
|
683
683
|
|
|
684
684
|
// src/text/keywordIndex.ts
|
|
685
685
|
var WORD_DELIMITERS = "[\\s,;:'\".!?()\\[\\]&\u2019`@-]";
|
|
@@ -734,17 +734,69 @@ function matchedLineExcerpt(text, keyword, options = {}) {
|
|
|
734
734
|
return `${start > 0 ? "\u2026" : ""}${slice}${end < line.length ? "\u2026" : ""}`;
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
-
// src/tools/
|
|
737
|
+
// src/tools/arguments.ts
|
|
738
738
|
import { z } from "zod";
|
|
739
|
+
var INVALID_INPUT = "invalid_input";
|
|
740
|
+
function strictInput(shape) {
|
|
741
|
+
const declared = Object.keys(shape);
|
|
742
|
+
const errorMap = (issue, ctx) => issue.code === z.ZodIssueCode.unrecognized_keys ? { message: unknownArgumentMessage(issue.keys, declared) } : { message: ctx.defaultError };
|
|
743
|
+
return z.strictObject(shape, { errorMap });
|
|
744
|
+
}
|
|
745
|
+
function unknownArgumentMessage(keys, declared) {
|
|
746
|
+
const named = keys.map((key) => {
|
|
747
|
+
const near = nearestArgument(key, declared);
|
|
748
|
+
return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;
|
|
749
|
+
}).join(", ");
|
|
750
|
+
return `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? "arguments" : "argument"} ${named}. This tool takes: ${declared.join(", ")}.`;
|
|
751
|
+
}
|
|
752
|
+
function nearestArgument(key, declared) {
|
|
753
|
+
const flatten = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
754
|
+
const flat = flatten(key);
|
|
755
|
+
if (flat.length === 0) return void 0;
|
|
756
|
+
const sameName = declared.find((name) => flatten(name) === flat);
|
|
757
|
+
if (sameName) return sameName;
|
|
758
|
+
const overlapping = declared.find((name) => {
|
|
759
|
+
const other = flatten(name);
|
|
760
|
+
const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];
|
|
761
|
+
return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));
|
|
762
|
+
});
|
|
763
|
+
if (overlapping) return overlapping;
|
|
764
|
+
let closest;
|
|
765
|
+
let shortest = Number.POSITIVE_INFINITY;
|
|
766
|
+
for (const name of declared) {
|
|
767
|
+
const distance = editDistance(flat, flatten(name));
|
|
768
|
+
if (distance < shortest) {
|
|
769
|
+
shortest = distance;
|
|
770
|
+
closest = name;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : void 0;
|
|
774
|
+
}
|
|
775
|
+
function editDistance(left, right) {
|
|
776
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
777
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
778
|
+
const current = [row];
|
|
779
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
780
|
+
const substitution = previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1);
|
|
781
|
+
current[column] = Math.min(substitution, previous[column] + 1, current[column - 1] + 1);
|
|
782
|
+
}
|
|
783
|
+
previous = current;
|
|
784
|
+
}
|
|
785
|
+
return previous[right.length];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/tools/shared.ts
|
|
789
|
+
import { z as z2 } from "zod";
|
|
739
790
|
var MAX_TEXT_MIRROR_CHARS = 1500;
|
|
740
|
-
var
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
791
|
+
var MAX_PAGE = 20;
|
|
792
|
+
var songResultSchema = z2.object({
|
|
793
|
+
id: z2.string().describe("lyrics.com song id. Pass this to get_lyrics."),
|
|
794
|
+
title: z2.string(),
|
|
795
|
+
artist: z2.string(),
|
|
796
|
+
album: z2.string().nullable(),
|
|
797
|
+
year: z2.number().int().nullable(),
|
|
798
|
+
source_url: z2.string().describe("Canonical lyrics.com page for this song."),
|
|
799
|
+
excerpt: z2.string().nullable().describe("Short lyric line showing where the query appears, when available.")
|
|
748
800
|
});
|
|
749
801
|
function toSongResultOut(song, excerpt) {
|
|
750
802
|
return {
|
|
@@ -757,9 +809,17 @@ function toSongResultOut(song, excerpt) {
|
|
|
757
809
|
excerpt
|
|
758
810
|
};
|
|
759
811
|
}
|
|
760
|
-
function
|
|
812
|
+
function indentMarkerLines(body) {
|
|
813
|
+
return body.replace(/^(Note:)/gm, " $1");
|
|
814
|
+
}
|
|
815
|
+
function ok(structured, text, notes = []) {
|
|
816
|
+
const trailer = notes.map((note) => `Note: ${note}`).join("\n");
|
|
817
|
+
const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);
|
|
818
|
+
const body = truncate(indentMarkerLines(text), Math.max(0, budget));
|
|
761
819
|
return {
|
|
762
|
-
content: [{ type: "text", text:
|
|
820
|
+
content: [{ type: "text", text: trailer ? `${body}
|
|
821
|
+
|
|
822
|
+
${trailer}` : body }],
|
|
763
823
|
structuredContent: structured
|
|
764
824
|
};
|
|
765
825
|
}
|
|
@@ -793,38 +853,39 @@ var getLyricsDescription = [
|
|
|
793
853
|
"lyrics field, which is a valid answer and not an error, so do not retry them.",
|
|
794
854
|
"Always cite 'source_url' and the artist when showing lyrics to a user."
|
|
795
855
|
].join(" ");
|
|
796
|
-
var
|
|
797
|
-
id:
|
|
798
|
-
url:
|
|
856
|
+
var getLyricsInput = strictInput({
|
|
857
|
+
id: z3.string().regex(/^\d+$/, "Song ids are digits only.").optional().describe("lyrics.com numeric song id, as returned by the search tools. Preferred over 'url'."),
|
|
858
|
+
url: z3.string().url().optional().describe(
|
|
799
859
|
"Full lyrics.com song URL. Only www.lyrics.com URLs are accepted. Ignored when 'id' is given."
|
|
800
860
|
),
|
|
801
|
-
max_chars:
|
|
802
|
-
offset:
|
|
803
|
-
highlight:
|
|
861
|
+
max_chars: z3.number().int().min(200).max(2e4).default(6e3).describe("Maximum characters of lyrics text to return in this call."),
|
|
862
|
+
offset: z3.number().int().min(0).default(0).describe("Character offset to resume from, for lyrics longer than max_chars."),
|
|
863
|
+
highlight: z3.string().max(120).optional().describe(
|
|
804
864
|
"Optional word. When set, the response reports whether it appears in the lyrics and on which line."
|
|
805
865
|
)
|
|
806
|
-
};
|
|
866
|
+
});
|
|
807
867
|
var getLyricsOutputShape = {
|
|
808
|
-
status:
|
|
809
|
-
id:
|
|
810
|
-
title:
|
|
811
|
-
artist:
|
|
812
|
-
source_url:
|
|
813
|
-
lyrics:
|
|
814
|
-
total_chars:
|
|
815
|
-
returned_chars:
|
|
816
|
-
offset:
|
|
817
|
-
next_offset:
|
|
818
|
-
truncated:
|
|
819
|
-
line_count:
|
|
820
|
-
highlight:
|
|
821
|
-
word:
|
|
822
|
-
found:
|
|
823
|
-
line_number:
|
|
824
|
-
line:
|
|
868
|
+
status: z3.enum(["ok", "no_lyrics"]),
|
|
869
|
+
id: z3.string(),
|
|
870
|
+
title: z3.string().nullable(),
|
|
871
|
+
artist: z3.string().nullable(),
|
|
872
|
+
source_url: z3.string(),
|
|
873
|
+
lyrics: z3.string().describe("Slice of the lyrics text. Empty when status is 'no_lyrics'."),
|
|
874
|
+
total_chars: z3.number().int(),
|
|
875
|
+
returned_chars: z3.number().int(),
|
|
876
|
+
offset: z3.number().int(),
|
|
877
|
+
next_offset: z3.number().int().nullable(),
|
|
878
|
+
truncated: z3.boolean(),
|
|
879
|
+
line_count: z3.number().int(),
|
|
880
|
+
highlight: z3.object({
|
|
881
|
+
word: z3.string(),
|
|
882
|
+
found: z3.boolean(),
|
|
883
|
+
line_number: z3.number().int().nullable(),
|
|
884
|
+
line: z3.string().nullable()
|
|
825
885
|
}).nullable(),
|
|
826
|
-
attribution:
|
|
827
|
-
source:
|
|
886
|
+
attribution: z3.string().describe("Ready-to-display credit line."),
|
|
887
|
+
source: z3.literal("lyrics.com"),
|
|
888
|
+
notes: z3.array(z3.string())
|
|
828
889
|
};
|
|
829
890
|
function sliceAtLineBoundary(text, offset, maxChars) {
|
|
830
891
|
if (offset >= text.length) return { slice: "", nextOffset: null };
|
|
@@ -840,7 +901,9 @@ async function runGetLyrics(client, args) {
|
|
|
840
901
|
const ref = {};
|
|
841
902
|
if (args.id) ref.id = args.id;
|
|
842
903
|
else if (args.url) ref.url = args.url;
|
|
843
|
-
const { data } = await client.getSong(ref);
|
|
904
|
+
const { data, cached } = await client.getSong(ref);
|
|
905
|
+
const notes = [];
|
|
906
|
+
if (cached) notes.push("Served from this server's short-lived in-memory cache.");
|
|
844
907
|
const credit = [data.title, data.artist].filter(Boolean).join(" \u2014 ");
|
|
845
908
|
const attribution = `${credit || "Lyrics"} via lyrics.com \u2014 ${data.url}`;
|
|
846
909
|
if (!data.hasLyrics) {
|
|
@@ -856,6 +919,7 @@ async function runGetLyrics(client, args) {
|
|
|
856
919
|
offset: 0,
|
|
857
920
|
next_offset: null,
|
|
858
921
|
truncated: false,
|
|
922
|
+
notes,
|
|
859
923
|
line_count: 0,
|
|
860
924
|
highlight: null,
|
|
861
925
|
attribution,
|
|
@@ -889,24 +953,31 @@ ${attribution}`
|
|
|
889
953
|
line: match?.line ?? null
|
|
890
954
|
} : null,
|
|
891
955
|
attribution,
|
|
892
|
-
source: "lyrics.com"
|
|
956
|
+
source: "lyrics.com",
|
|
957
|
+
notes
|
|
893
958
|
};
|
|
959
|
+
const pastTheEnd = args.offset > 0 && slice === "" && data.lyrics.length > 0;
|
|
960
|
+
if (pastTheEnd) {
|
|
961
|
+
notes.push(
|
|
962
|
+
`offset=${args.offset} is past the end of a body of ${data.lyrics.length} characters. Call again with offset=0 to read it from the start.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
894
965
|
const summary = [
|
|
895
966
|
attribution,
|
|
896
967
|
`${structured.line_count} lines, ${structured.total_chars} characters.`,
|
|
897
|
-
nextOffset !== null ? `Truncated: call again with offset=${nextOffset} to continue.` : "Complete.",
|
|
968
|
+
pastTheEnd ? `Nothing at offset=${args.offset}: that is past the end. Call again with offset=0.` : nextOffset !== null ? `Truncated: call again with offset=${nextOffset} to continue.` : "Complete.",
|
|
898
969
|
structured.highlight ? structured.highlight.found ? `"${structured.highlight.word}" appears on line ${structured.highlight.line_number}.` : `"${structured.highlight.word}" does not appear in these lyrics.` : ""
|
|
899
970
|
].filter(Boolean).join("\n");
|
|
900
971
|
return ok(structured, truncate(`${summary}
|
|
901
972
|
|
|
902
|
-
${slice}`, 4e3));
|
|
973
|
+
${slice}`, 4e3), notes);
|
|
903
974
|
} catch (error) {
|
|
904
975
|
return toToolError(error);
|
|
905
976
|
}
|
|
906
977
|
}
|
|
907
978
|
|
|
908
979
|
// src/tools/searchLyrics.ts
|
|
909
|
-
import { z as
|
|
980
|
+
import { z as z4 } from "zod";
|
|
910
981
|
|
|
911
982
|
// src/text/dedupe.ts
|
|
912
983
|
function songKey(artist, title) {
|
|
@@ -938,25 +1009,25 @@ var searchLyricsDescription = [
|
|
|
938
1009
|
"lyrics.com also returns loose and title-only matches, so results are filtered locally to keep only songs",
|
|
939
1010
|
`where the word genuinely appears in the lyrics. Set 'verify' to "none" to see the raw, unfiltered list.`
|
|
940
1011
|
].join(" ");
|
|
941
|
-
var
|
|
942
|
-
query:
|
|
943
|
-
limit:
|
|
944
|
-
page:
|
|
945
|
-
verify:
|
|
1012
|
+
var searchLyricsInput = strictInput({
|
|
1013
|
+
query: z4.string().min(1).max(120).describe("Word or short phrase to look for inside the lyrics, for example 'autrefois'."),
|
|
1014
|
+
limit: z4.number().int().min(1).max(50).default(10).describe("Maximum songs to return from this page."),
|
|
1015
|
+
page: z4.number().int().min(1).max(20).default(1).describe("Result page on lyrics.com. Each page holds up to 24 raw results."),
|
|
1016
|
+
verify: z4.enum(["snippet", "full", "none"]).default("snippet").describe(
|
|
946
1017
|
"How to confirm the query really appears in the lyrics. 'snippet' checks the excerpt lyrics.com already returned and costs nothing. 'full' fetches up to 5 song pages and checks the complete lyrics, which is slow and can trigger rate limiting. 'none' disables filtering."
|
|
947
1018
|
),
|
|
948
|
-
include_excerpt:
|
|
949
|
-
};
|
|
1019
|
+
include_excerpt: z4.boolean().default(true).describe("Include the matching lyric line excerpt in each result.")
|
|
1020
|
+
});
|
|
950
1021
|
var searchLyricsOutputShape = {
|
|
951
|
-
query:
|
|
952
|
-
page:
|
|
953
|
-
results:
|
|
954
|
-
raw_result_count:
|
|
955
|
-
filtered_out:
|
|
956
|
-
has_more:
|
|
957
|
-
next_page:
|
|
958
|
-
source:
|
|
959
|
-
notes:
|
|
1022
|
+
query: z4.string(),
|
|
1023
|
+
page: z4.number().int(),
|
|
1024
|
+
results: z4.array(songResultSchema),
|
|
1025
|
+
raw_result_count: z4.number().int().describe("Rows lyrics.com returned before local filtering and deduplication."),
|
|
1026
|
+
filtered_out: z4.number().int().describe("Rows dropped because the query was not found in their lyrics."),
|
|
1027
|
+
has_more: z4.boolean(),
|
|
1028
|
+
next_page: z4.number().int().nullable(),
|
|
1029
|
+
source: z4.literal("lyrics.com"),
|
|
1030
|
+
notes: z4.array(z4.string()).describe("Caveats worth knowing, such as a cache hit or partial verification.")
|
|
960
1031
|
};
|
|
961
1032
|
async function runSearchLyrics(client, args) {
|
|
962
1033
|
try {
|
|
@@ -1006,14 +1077,16 @@ async function runSearchLyrics(client, args) {
|
|
|
1006
1077
|
raw_result_count: data.rawCount,
|
|
1007
1078
|
filtered_out: filteredOut,
|
|
1008
1079
|
has_more: data.hasMore,
|
|
1009
|
-
next_page: data.hasMore ? args.page + 1 : null,
|
|
1080
|
+
next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,
|
|
1010
1081
|
source: "lyrics.com",
|
|
1011
1082
|
notes
|
|
1012
1083
|
};
|
|
1013
1084
|
const header = results.length > 0 ? `${results.length} song(s) on page ${args.page} for "${args.query}":` : `No song on page ${args.page} matched "${args.query}".`;
|
|
1014
|
-
const footer = data.hasMore ? `
|
|
1085
|
+
const footer = data.hasMore && args.page < MAX_PAGE ? `
|
|
1086
|
+
|
|
1087
|
+
More results available: call again with page=${args.page + 1}.` : data.hasMore ? `
|
|
1015
1088
|
|
|
1016
|
-
|
|
1089
|
+
lyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.` : "";
|
|
1017
1090
|
return ok(structured, `${header}
|
|
1018
1091
|
${renderResultList(results)}${footer}`);
|
|
1019
1092
|
} catch (error) {
|
|
@@ -1022,7 +1095,7 @@ ${renderResultList(results)}${footer}`);
|
|
|
1022
1095
|
}
|
|
1023
1096
|
|
|
1024
1097
|
// src/tools/searchSongs.ts
|
|
1025
|
-
import { z as
|
|
1098
|
+
import { z as z5 } from "zod";
|
|
1026
1099
|
var searchSongsDescription = [
|
|
1027
1100
|
"Find songs on lyrics.com by their title.",
|
|
1028
1101
|
"Returns candidate songs with artist, album, year, and the lyrics.com id and URL.",
|
|
@@ -1031,26 +1104,26 @@ var searchSongsDescription = [
|
|
|
1031
1104
|
"lyrics.com searches titles and lyrics together, so results are ranked and filtered locally by title match;",
|
|
1032
1105
|
'use match="strict" to keep only genuine title matches.'
|
|
1033
1106
|
].join(" ");
|
|
1034
|
-
var
|
|
1035
|
-
title:
|
|
1036
|
-
artist:
|
|
1037
|
-
limit:
|
|
1038
|
-
page:
|
|
1039
|
-
match:
|
|
1107
|
+
var searchSongsInput = strictInput({
|
|
1108
|
+
title: z5.string().min(1).max(120).describe("Song title, or part of it."),
|
|
1109
|
+
artist: z5.string().max(120).optional().describe("Optional artist name, matched case-insensitively against each result."),
|
|
1110
|
+
limit: z5.number().int().min(1).max(50).default(10),
|
|
1111
|
+
page: z5.number().int().min(1).max(20).default(1),
|
|
1112
|
+
match: z5.enum(["loose", "strict"]).default("loose").describe(
|
|
1040
1113
|
"'strict' keeps only results whose title matches the query as a whole title, ignoring suffixes such as '(Remastered)'. 'loose' keeps anything lyrics.com returned, best match first."
|
|
1041
1114
|
)
|
|
1042
|
-
};
|
|
1115
|
+
});
|
|
1043
1116
|
var searchSongsOutputShape = {
|
|
1044
|
-
title:
|
|
1045
|
-
artist_filter:
|
|
1046
|
-
page:
|
|
1047
|
-
results:
|
|
1048
|
-
raw_result_count:
|
|
1049
|
-
filtered_out:
|
|
1050
|
-
has_more:
|
|
1051
|
-
next_page:
|
|
1052
|
-
source:
|
|
1053
|
-
notes:
|
|
1117
|
+
title: z5.string(),
|
|
1118
|
+
artist_filter: z5.string().nullable(),
|
|
1119
|
+
page: z5.number().int(),
|
|
1120
|
+
results: z5.array(songResultSchema),
|
|
1121
|
+
raw_result_count: z5.number().int(),
|
|
1122
|
+
filtered_out: z5.number().int(),
|
|
1123
|
+
has_more: z5.boolean(),
|
|
1124
|
+
next_page: z5.number().int().nullable(),
|
|
1125
|
+
source: z5.literal("lyrics.com"),
|
|
1126
|
+
notes: z5.array(z5.string())
|
|
1054
1127
|
};
|
|
1055
1128
|
function normalizeTitle(title) {
|
|
1056
1129
|
return title.replace(/\s*[[(].*?[\])]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
@@ -1097,16 +1170,24 @@ async function runSearchSongs(client, args) {
|
|
|
1097
1170
|
raw_result_count: data.rawCount,
|
|
1098
1171
|
filtered_out: filteredOut,
|
|
1099
1172
|
has_more: data.hasMore,
|
|
1100
|
-
next_page: data.hasMore ? args.page + 1 : null,
|
|
1173
|
+
next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,
|
|
1101
1174
|
source: "lyrics.com",
|
|
1102
1175
|
notes
|
|
1103
1176
|
};
|
|
1104
|
-
const
|
|
1105
|
-
const
|
|
1177
|
+
const loose = args.match !== "strict";
|
|
1178
|
+
const header = results.length > 0 ? loose ? `${results.length} song(s) lyrics.com offers on page ${args.page} for "${args.title}":` : `${results.length} song(s) on page ${args.page} matching "${args.title}":` : `No song on page ${args.page} matched "${args.title}".`;
|
|
1179
|
+
const footer = data.hasMore && args.page < MAX_PAGE ? `
|
|
1180
|
+
|
|
1181
|
+
More results available: call again with page=${args.page + 1}.` : data.hasMore ? `
|
|
1106
1182
|
|
|
1107
|
-
|
|
1183
|
+
lyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.` : "";
|
|
1184
|
+
if (results.length > 0 && loose) {
|
|
1185
|
+
notes.push(
|
|
1186
|
+
`These are lyrics.com's own suggestions for "${args.title}", not rows whose title matches it. Check each title before quoting one, or ask again with match="strict".`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1108
1189
|
return ok(structured, `${header}
|
|
1109
|
-
${renderResultList(results)}${footer}
|
|
1190
|
+
${renderResultList(results)}${footer}`, notes);
|
|
1110
1191
|
} catch (error) {
|
|
1111
1192
|
return toToolError(error);
|
|
1112
1193
|
}
|
|
@@ -1138,8 +1219,8 @@ function createServer(options = {}) {
|
|
|
1138
1219
|
{
|
|
1139
1220
|
title: "Search lyrics by word",
|
|
1140
1221
|
description: searchLyricsDescription,
|
|
1141
|
-
inputSchema:
|
|
1142
|
-
outputSchema:
|
|
1222
|
+
inputSchema: searchLyricsInput,
|
|
1223
|
+
outputSchema: z6.object(searchLyricsOutputShape),
|
|
1143
1224
|
annotations: READ_ONLY
|
|
1144
1225
|
},
|
|
1145
1226
|
async (args) => runSearchLyrics(client, args)
|
|
@@ -1149,8 +1230,8 @@ function createServer(options = {}) {
|
|
|
1149
1230
|
{
|
|
1150
1231
|
title: "Search songs by title",
|
|
1151
1232
|
description: searchSongsDescription,
|
|
1152
|
-
inputSchema:
|
|
1153
|
-
outputSchema:
|
|
1233
|
+
inputSchema: searchSongsInput,
|
|
1234
|
+
outputSchema: z6.object(searchSongsOutputShape),
|
|
1154
1235
|
annotations: READ_ONLY
|
|
1155
1236
|
},
|
|
1156
1237
|
async (args) => runSearchSongs(client, args)
|
|
@@ -1160,8 +1241,8 @@ function createServer(options = {}) {
|
|
|
1160
1241
|
{
|
|
1161
1242
|
title: "Get full lyrics",
|
|
1162
1243
|
description: getLyricsDescription,
|
|
1163
|
-
inputSchema:
|
|
1164
|
-
outputSchema:
|
|
1244
|
+
inputSchema: getLyricsInput,
|
|
1245
|
+
outputSchema: z6.object(getLyricsOutputShape),
|
|
1165
1246
|
annotations: READ_ONLY
|
|
1166
1247
|
},
|
|
1167
1248
|
async (args) => runGetLyrics(client, args)
|