art-institute-mcp 1.0.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 ADDED
@@ -0,0 +1,22 @@
1
+ # art-institute-mcp
2
+
3
+ Art Institute of Chicago collection search.
4
+
5
+ A merged MCP server that consolidates duplicate single-purpose servers in this monorepo into one focused server.
6
+
7
+ ## Tools
8
+
9
+ - `search` — Search Art Institute of Chicago artworks.
10
+ - `artwork` — Details for one artwork.
11
+
12
+ ## Run
13
+
14
+ ```bash
15
+ npm install
16
+ npm run build
17
+ node dist/index.js
18
+ ```
19
+
20
+ ## Source
21
+
22
+ Public free APIs only. See `src/api.ts` for exact endpoints.
package/dist/api.js ADDED
@@ -0,0 +1,113 @@
1
+ const m0 = (() => {
2
+ const BASE = 'https://api.artic.edu/api/v1';
3
+ function pick(obj, key) {
4
+ return obj?.[key] ?? '';
5
+ }
6
+ function firstText(value) {
7
+ if (typeof value === 'string')
8
+ return value;
9
+ if (Array.isArray(value))
10
+ return value.map((v) => (typeof v === 'string' ? v : '')).filter(Boolean).join(', ');
11
+ return '';
12
+ }
13
+ async function search(args) {
14
+ const q = (args.query ?? '').trim();
15
+ if (!q)
16
+ return 'Provide a search query.';
17
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 30));
18
+ const url = `${BASE}/artworks/search?q=${encodeURIComponent(q)}&limit=${limit}&fields=id,title,artist_title,date_display,image_id`;
19
+ const res = await fetch(url, {
20
+ headers: { 'User-Agent': 'mrfentmen-art-institute-mcp/1.0', Accept: 'application/json' },
21
+ signal: AbortSignal.timeout(20000),
22
+ });
23
+ if (!res.ok)
24
+ throw new Error(`Art Institute search returned ${res.status}`);
25
+ const data = (await res.json());
26
+ const rows = (data.data ?? []).map((d) => {
27
+ const o = d;
28
+ const imageId = pick(o, 'image_id');
29
+ return `${pick(o, 'title')} | ${firstText(pick(o, 'artist_title'))} | ${pick(o, 'date_display')} | id ${pick(o, 'id')}${imageId ? ` | image https://www.artic.edu/iiif/2/${imageId}/full/843,/0/default.jpg` : ''}`;
30
+ });
31
+ if (!rows.length)
32
+ return `No artworks found for "${q}".`;
33
+ return `Art Institute results for "${q}" (${rows.length} shown):\n` + rows.map((r, i) => `${i + 1}. ${r}`).join('\n');
34
+ }
35
+ async function artwork(args) {
36
+ const url = `${BASE}/artworks/${args.id}?fields=id,title,artist_title,date_display,medium_display,description,image_id`;
37
+ const res = await fetch(url, {
38
+ headers: { 'User-Agent': 'mrfentmen-art-institute-mcp/1.0', Accept: 'application/json' },
39
+ signal: AbortSignal.timeout(20000),
40
+ });
41
+ if (!res.ok)
42
+ throw new Error(`Art Institute fetch returned ${res.status}`);
43
+ const data = (await res.json());
44
+ const o = (data.data ?? {});
45
+ const imageId = pick(o, 'image_id');
46
+ const lines = [
47
+ `Title: ${pick(o, 'title')}`,
48
+ `Artist: ${firstText(pick(o, 'artist_title'))}`,
49
+ `Date: ${pick(o, 'date_display')}`,
50
+ `Medium: ${pick(o, 'medium_display')}`,
51
+ ];
52
+ const desc = pick(o, 'description');
53
+ if (desc)
54
+ lines.push(`Description: ${desc.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400)}`);
55
+ if (imageId)
56
+ lines.push(`Image: https://www.artic.edu/iiif/2/${imageId}/full/843,/0/default.jpg`);
57
+ return lines.join('\n');
58
+ }
59
+ return { artwork, search };
60
+ })();
61
+ const m1 = (() => {
62
+ const BASE = 'https://api.artic.edu/api/v1/artworks';
63
+ async function search(args) {
64
+ const query = (args.query ?? '').trim();
65
+ if (!query)
66
+ return 'Provide search terms.';
67
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 50));
68
+ const res = await fetch(`${BASE}/search?q=${encodeURIComponent(query)}&limit=${limit}&fields=id,title,artist_title,date_display,image_id`, {
69
+ headers: { 'User-Agent': 'mrfentmen-artic-mcp/1.0', Accept: 'application/json' },
70
+ signal: AbortSignal.timeout(20000),
71
+ });
72
+ if (!res.ok)
73
+ throw new Error(`Artic returned ${res.status}`);
74
+ const d = (await res.json());
75
+ const rows = d.data ?? [];
76
+ if (!rows.length)
77
+ return 'No artworks found.';
78
+ return `Artworks for "${query}" (${rows.length} shown):\n` +
79
+ rows.map((r, i) => {
80
+ const s = (k) => (r[k] != null ? String(r[k]) : '');
81
+ return `${i + 1}. ${s('title')}${s('artist_title') ? ` by ${s('artist_title')}` : ''}${s('date_display') ? ` (${s('date_display')})` : ''}`;
82
+ }).join('\n');
83
+ }
84
+ async function artwork(args) {
85
+ const id = Math.floor(args.id ?? 0);
86
+ if (!id)
87
+ return 'Provide an artwork id.';
88
+ const res = await fetch(`${BASE}/${id}?fields=id,title,artist_title,date_display,medium_display,description,image_id`, {
89
+ headers: { 'User-Agent': 'mrfentmen-artic-mcp/1.0', Accept: 'application/json' },
90
+ signal: AbortSignal.timeout(20000),
91
+ });
92
+ if (!res.ok)
93
+ throw new Error(`Artic returned ${res.status}`);
94
+ const d = (await res.json());
95
+ const r = d.data;
96
+ if (!r)
97
+ return `No artwork with id ${id}.`;
98
+ const s = (k) => (r[k] != null ? String(r[k]) : '');
99
+ return [
100
+ `${s('title')}${s('artist_title') ? ` by ${s('artist_title')}` : ''}`,
101
+ s('date_display') ? `Date: ${s('date_display')}` : '',
102
+ s('medium_display') ? `Medium: ${s('medium_display')}` : '',
103
+ s('description') ? `\n${s('description').replace(/<[^>]+>/g, '')}` : '',
104
+ ].filter(Boolean).join('\n');
105
+ }
106
+ return { artwork, search };
107
+ })();
108
+ export const artwork = m0.artwork;
109
+ export const search = m0.search;
110
+ export const m0_search = m0.search;
111
+ export const m0_artwork = m0.artwork;
112
+ export const m1_search = m1.search;
113
+ export const m1_artwork = m1.artwork;
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ const main = async () => { const server = createServer(); await server.connect(new StdioServerTransport()); };
4
+ main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });
package/dist/server.js ADDED
@@ -0,0 +1,38 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { m0_artwork, m0_search } from './api.js';
4
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ const error = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
8
+ const errorMessage = error;
9
+ export function createServer() {
10
+ const server = new McpServer({ name: 'art-institute-mcp', version: '1.0.0' });
11
+ server.registerTool("search", {
12
+ title: "Search",
13
+ description: "Search artworks by text.",
14
+ inputSchema: z.object({ query: z.string().describe("Search terms."), limit: z.number().describe("Max results.").optional() }),
15
+ annotations: READ_ONLY,
16
+ }, async (args) => {
17
+ try {
18
+ return text(await m0_search(args));
19
+ }
20
+ catch (e) {
21
+ return textError(error(e));
22
+ }
23
+ });
24
+ server.registerTool("artwork", {
25
+ title: "Artwork",
26
+ description: "Details for one artwork.",
27
+ inputSchema: z.object({ id: z.number().describe("Artwork ID.") }),
28
+ annotations: READ_ONLY,
29
+ }, async (args) => {
30
+ try {
31
+ return text(await m0_artwork(args));
32
+ }
33
+ catch (e) {
34
+ return textError(error(e));
35
+ }
36
+ });
37
+ return server;
38
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "art-institute-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Art Institute of Chicago collection search.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.0.0",
13
+ "zod": "^3.23.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.5.0",
17
+ "@types/node": "^20.0.0"
18
+ },
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "mcpName": "io.github.mrfentmen/art-institute-mcp"
24
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/art-institute-mcp",
4
+ "description": "Art Institute of Chicago collection search.",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/awesome-mcps",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "art-institute-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }
package/src/api.ts ADDED
@@ -0,0 +1,132 @@
1
+
2
+ export interface m0_SearchArgs {
3
+ query: string;
4
+ limit?: number;
5
+ }
6
+
7
+ export interface m0_ArtworkArgs {
8
+ id: number;
9
+ }
10
+
11
+ export interface m1_SearchArgs {
12
+ query: string;
13
+ limit?: number;
14
+ }
15
+
16
+ export interface m1_ArtworkArgs {
17
+ id: number;
18
+ }
19
+
20
+ const m0 = (() => {
21
+ const BASE = 'https://api.artic.edu/api/v1';
22
+
23
+
24
+
25
+ function pick<T>(obj: Record<string, unknown>, key: string): T {
26
+ return (obj?.[key] as T) ?? ('' as T);
27
+ }
28
+
29
+ function firstText(value: unknown): string {
30
+ if (typeof value === 'string') return value;
31
+ if (Array.isArray(value)) return value.map((v) => (typeof v === 'string' ? v : '')).filter(Boolean).join(', ');
32
+ return '';
33
+ }
34
+
35
+ async function search(args: m0_SearchArgs): Promise<string> {
36
+ const q = (args.query ?? '').trim();
37
+ if (!q) return 'Provide a search query.';
38
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 30));
39
+ const url = `${BASE}/artworks/search?q=${encodeURIComponent(q)}&limit=${limit}&fields=id,title,artist_title,date_display,image_id`;
40
+ const res = await fetch(url, {
41
+ headers: { 'User-Agent': 'mrfentmen-art-institute-mcp/1.0', Accept: 'application/json' },
42
+ signal: AbortSignal.timeout(20000),
43
+ });
44
+ if (!res.ok) throw new Error(`Art Institute search returned ${res.status}`);
45
+ const data = (await res.json()) as { data?: unknown[] };
46
+ const rows = (data.data ?? []).map((d) => {
47
+ const o = d as Record<string, unknown>;
48
+ const imageId = pick<string>(o, 'image_id');
49
+ return `${pick<string>(o, 'title')} | ${firstText(pick(o, 'artist_title'))} | ${pick<string>(o, 'date_display')} | id ${pick<number>(o, 'id')}${imageId ? ` | image https://www.artic.edu/iiif/2/${imageId}/full/843,/0/default.jpg` : ''}`;
50
+ });
51
+ if (!rows.length) return `No artworks found for "${q}".`;
52
+ return `Art Institute results for "${q}" (${rows.length} shown):\n` + rows.map((r, i) => `${i + 1}. ${r}`).join('\n');
53
+ }
54
+
55
+ async function artwork(args: m0_ArtworkArgs): Promise<string> {
56
+ const url = `${BASE}/artworks/${args.id}?fields=id,title,artist_title,date_display,medium_display,description,image_id`;
57
+ const res = await fetch(url, {
58
+ headers: { 'User-Agent': 'mrfentmen-art-institute-mcp/1.0', Accept: 'application/json' },
59
+ signal: AbortSignal.timeout(20000),
60
+ });
61
+ if (!res.ok) throw new Error(`Art Institute fetch returned ${res.status}`);
62
+ const data = (await res.json()) as { data?: unknown };
63
+ const o = (data.data ?? {}) as Record<string, unknown>;
64
+ const imageId = pick<string>(o, 'image_id');
65
+ const lines = [
66
+ `Title: ${pick<string>(o, 'title')}`,
67
+ `Artist: ${firstText(pick(o, 'artist_title'))}`,
68
+ `Date: ${pick<string>(o, 'date_display')}`,
69
+ `Medium: ${pick<string>(o, 'medium_display')}`,
70
+ ];
71
+ const desc = pick<string>(o, 'description');
72
+ if (desc) lines.push(`Description: ${desc.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400)}`);
73
+ if (imageId) lines.push(`Image: https://www.artic.edu/iiif/2/${imageId}/full/843,/0/default.jpg`);
74
+ return lines.join('\n');
75
+ }
76
+
77
+ return { artwork, search };
78
+ })();
79
+
80
+ const m1 = (() => {
81
+ const BASE = 'https://api.artic.edu/api/v1/artworks';
82
+
83
+
84
+
85
+ async function search(args: m1_SearchArgs): Promise<string> {
86
+ const query = (args.query ?? '').trim();
87
+ if (!query) return 'Provide search terms.';
88
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 50));
89
+ const res = await fetch(`${BASE}/search?q=${encodeURIComponent(query)}&limit=${limit}&fields=id,title,artist_title,date_display,image_id`, {
90
+ headers: { 'User-Agent': 'mrfentmen-artic-mcp/1.0', Accept: 'application/json' },
91
+ signal: AbortSignal.timeout(20000),
92
+ });
93
+ if (!res.ok) throw new Error(`Artic returned ${res.status}`);
94
+ const d = (await res.json()) as { data?: Array<Record<string, unknown>> };
95
+ const rows = d.data ?? [];
96
+ if (!rows.length) return 'No artworks found.';
97
+ return `Artworks for "${query}" (${rows.length} shown):\n` +
98
+ rows.map((r, i) => {
99
+ const s = (k: string) => (r[k] != null ? String(r[k]) : '');
100
+ return `${i + 1}. ${s('title')}${s('artist_title') ? ` by ${s('artist_title')}` : ''}${s('date_display') ? ` (${s('date_display')})` : ''}`;
101
+ }).join('\n');
102
+ }
103
+
104
+ async function artwork(args: m1_ArtworkArgs): Promise<string> {
105
+ const id = Math.floor(args.id ?? 0);
106
+ if (!id) return 'Provide an artwork id.';
107
+ const res = await fetch(`${BASE}/${id}?fields=id,title,artist_title,date_display,medium_display,description,image_id`, {
108
+ headers: { 'User-Agent': 'mrfentmen-artic-mcp/1.0', Accept: 'application/json' },
109
+ signal: AbortSignal.timeout(20000),
110
+ });
111
+ if (!res.ok) throw new Error(`Artic returned ${res.status}`);
112
+ const d = (await res.json()) as { data?: Record<string, unknown> };
113
+ const r = d.data;
114
+ if (!r) return `No artwork with id ${id}.`;
115
+ const s = (k: string) => (r[k] != null ? String(r[k]) : '');
116
+ return [
117
+ `${s('title')}${s('artist_title') ? ` by ${s('artist_title')}` : ''}`,
118
+ s('date_display') ? `Date: ${s('date_display')}` : '',
119
+ s('medium_display') ? `Medium: ${s('medium_display')}` : '',
120
+ s('description') ? `\n${s('description').replace(/<[^>]+>/g, '')}` : '',
121
+ ].filter(Boolean).join('\n');
122
+ }
123
+
124
+ return { artwork, search };
125
+ })();
126
+
127
+ export const artwork = m0.artwork;
128
+ export const search = m0.search;
129
+ export const m0_search = m0.search;
130
+ export const m0_artwork = m0.artwork;
131
+ export const m1_search = m1.search;
132
+ export const m1_artwork = m1.artwork;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
+ import { createServer } from "./server.js"
3
+ const main = async () => { const server = createServer(); await server.connect(new StdioServerTransport()) }
4
+ main().catch((error) => { console.error("Fatal error:", error); process.exit(1) })
package/src/server.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { m0_artwork, m0_search } from './api.js'
4
+
5
+ const text = (value: string) => ({ content: [{ type: 'text' as const, text: value }] })
6
+ const textError = (t: string) => ({ content: [{ type: "text" as const, text: t }], isError: true as const })
7
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true } as const
8
+ const error = (e: unknown) => `Error: ${e instanceof Error ? e.message : String(e)}`
9
+ const errorMessage = error
10
+
11
+ export function createServer(): McpServer {
12
+ const server = new McpServer({ name: 'art-institute-mcp', version: '1.0.0' })
13
+ server.registerTool(
14
+ "search",
15
+ {
16
+ title: "Search",
17
+ description: "Search artworks by text.",
18
+ inputSchema: z.object( { query: z.string().describe("Search terms."), limit: z.number().describe("Max results.").optional() }),
19
+ annotations: READ_ONLY,
20
+ },
21
+ async (args) => {
22
+ try { return text(await m0_search(args)) } catch (e) { return textError(error(e)) }
23
+ }
24
+ )
25
+ server.registerTool(
26
+ "artwork",
27
+ {
28
+ title: "Artwork",
29
+ description: "Details for one artwork.",
30
+ inputSchema: z.object( { id: z.number().describe("Artwork ID.") }),
31
+ annotations: READ_ONLY,
32
+ },
33
+ async (args) => {
34
+ try { return text(await m0_artwork(args)) } catch (e) { return textError(error(e)) }
35
+ }
36
+ )
37
+ return server
38
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": [
14
+ "src/**/*"
15
+ ]
16
+ }