open-museum-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 +21 -0
- package/README.md +216 -0
- package/dist/cite.d.ts +17 -0
- package/dist/cite.js +61 -0
- package/dist/cite.js.map +1 -0
- package/dist/data/dynasties.json +93 -0
- package/dist/data/regions.json +21 -0
- package/dist/dateParser.d.ts +17 -0
- package/dist/dateParser.js +265 -0
- package/dist/dateParser.js.map +1 -0
- package/dist/db.d.ts +35 -0
- package/dist/db.js +182 -0
- package/dist/db.js.map +1 -0
- package/dist/fetchers/met.d.ts +2 -0
- package/dist/fetchers/met.js +132 -0
- package/dist/fetchers/met.js.map +1 -0
- package/dist/fetchers/types.d.ts +11 -0
- package/dist/fetchers/types.js +2 -0
- package/dist/fetchers/types.js.map +1 -0
- package/dist/licenseGate.d.ts +13 -0
- package/dist/licenseGate.js +112 -0
- package/dist/licenseGate.js.map +1 -0
- package/dist/mappings.d.ts +3 -0
- package/dist/mappings.js +36 -0
- package/dist/mappings.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +243 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ArtworkLicense } from './types.js';
|
|
2
|
+
export interface LicenseDecision {
|
|
3
|
+
accepted: boolean;
|
|
4
|
+
license: ArtworkLicense | null;
|
|
5
|
+
imageOpenAccess: boolean;
|
|
6
|
+
metadataOpenAccess: boolean;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
export type LicenseValidator = (raw: unknown) => LicenseDecision;
|
|
10
|
+
export declare const validateMetLicense: LicenseValidator;
|
|
11
|
+
export declare const validateClevelandLicense: LicenseValidator;
|
|
12
|
+
export declare const validateAicLicense: LicenseValidator;
|
|
13
|
+
export declare function validateLicense(museumCode: string, raw: unknown): LicenseDecision;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
function nowIso() {
|
|
2
|
+
return new Date().toISOString();
|
|
3
|
+
}
|
|
4
|
+
function reject(reason) {
|
|
5
|
+
return {
|
|
6
|
+
accepted: false,
|
|
7
|
+
license: null,
|
|
8
|
+
imageOpenAccess: false,
|
|
9
|
+
metadataOpenAccess: false,
|
|
10
|
+
reason,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// The Met's `isPublicDomain` boolean is the museum's per-object marker for
|
|
14
|
+
// images released under CC0. We accept it as sufficient for both
|
|
15
|
+
// imageOpenAccess and metadataOpenAccess because that is what the Met itself
|
|
16
|
+
// publishes. This is not an independent rights audit — third-party content,
|
|
17
|
+
// model releases, or culturally sensitive material may carry obligations the
|
|
18
|
+
// museum's representation does not surface. See README "Disclaimer".
|
|
19
|
+
export const validateMetLicense = (raw) => {
|
|
20
|
+
if (!raw || typeof raw !== 'object') {
|
|
21
|
+
return reject('met: object missing or not an object');
|
|
22
|
+
}
|
|
23
|
+
const obj = raw;
|
|
24
|
+
const isPD = obj.isPublicDomain;
|
|
25
|
+
if (isPD === true) {
|
|
26
|
+
return {
|
|
27
|
+
accepted: true,
|
|
28
|
+
license: {
|
|
29
|
+
type: 'CC0',
|
|
30
|
+
rawValue: 'true',
|
|
31
|
+
verificationSource: 'met.isPublicDomain',
|
|
32
|
+
verifiedAt: nowIso(),
|
|
33
|
+
confidence: 'high',
|
|
34
|
+
},
|
|
35
|
+
imageOpenAccess: true,
|
|
36
|
+
metadataOpenAccess: true,
|
|
37
|
+
reason: 'met: isPublicDomain=true',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (isPD === false) {
|
|
41
|
+
return reject('met: isPublicDomain=false');
|
|
42
|
+
}
|
|
43
|
+
return reject('met: isPublicDomain field missing or non-boolean (strict default reject)');
|
|
44
|
+
};
|
|
45
|
+
export const validateClevelandLicense = (raw) => {
|
|
46
|
+
if (!raw || typeof raw !== 'object') {
|
|
47
|
+
return reject('cleveland: object missing or not an object');
|
|
48
|
+
}
|
|
49
|
+
const obj = raw;
|
|
50
|
+
const status = obj.share_license_status;
|
|
51
|
+
if (typeof status === 'string' && status.toUpperCase() === 'CC0') {
|
|
52
|
+
return {
|
|
53
|
+
accepted: true,
|
|
54
|
+
license: {
|
|
55
|
+
type: 'CC0',
|
|
56
|
+
rawValue: status,
|
|
57
|
+
verificationSource: 'cleveland.share_license_status',
|
|
58
|
+
verifiedAt: nowIso(),
|
|
59
|
+
confidence: 'high',
|
|
60
|
+
},
|
|
61
|
+
imageOpenAccess: true,
|
|
62
|
+
metadataOpenAccess: true,
|
|
63
|
+
reason: 'cleveland: share_license_status=CC0',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return reject(`cleveland: share_license_status=${typeof status === 'string' ? status : 'missing'} (strict default reject)`);
|
|
67
|
+
};
|
|
68
|
+
// The Art Institute of Chicago's API returns is_public_domain as a per-object
|
|
69
|
+
// boolean. AIC's documentation explicitly notes that the API's CC0 framing
|
|
70
|
+
// covers the catalog data, while image reuse rights are described separately
|
|
71
|
+
// in their image licensing materials. The per-object boolean does mark images
|
|
72
|
+
// they release under CC0, so we accept it for imageOpenAccess — but the
|
|
73
|
+
// distinction matters: AIC's docs caution that even CC0-marked content may
|
|
74
|
+
// involve third-party permissions or culturally sensitive material. We
|
|
75
|
+
// surface this caveat in the README "Disclaimer" rather than downgrading
|
|
76
|
+
// confidence here, because the museum's own representation is unambiguous.
|
|
77
|
+
export const validateAicLicense = (raw) => {
|
|
78
|
+
if (!raw || typeof raw !== 'object') {
|
|
79
|
+
return reject('aic: object missing or not an object');
|
|
80
|
+
}
|
|
81
|
+
const obj = raw;
|
|
82
|
+
const isPD = obj.is_public_domain;
|
|
83
|
+
if (isPD === true) {
|
|
84
|
+
return {
|
|
85
|
+
accepted: true,
|
|
86
|
+
license: {
|
|
87
|
+
type: 'CC0',
|
|
88
|
+
rawValue: 'true',
|
|
89
|
+
verificationSource: 'aic.is_public_domain',
|
|
90
|
+
verifiedAt: nowIso(),
|
|
91
|
+
confidence: 'high',
|
|
92
|
+
},
|
|
93
|
+
imageOpenAccess: true,
|
|
94
|
+
metadataOpenAccess: true,
|
|
95
|
+
reason: 'aic: is_public_domain=true',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return reject(`aic: is_public_domain=${isPD} (strict default reject)`);
|
|
99
|
+
};
|
|
100
|
+
const VALIDATORS = {
|
|
101
|
+
met: validateMetLicense,
|
|
102
|
+
cleveland: validateClevelandLicense,
|
|
103
|
+
aic: validateAicLicense,
|
|
104
|
+
};
|
|
105
|
+
export function validateLicense(museumCode, raw) {
|
|
106
|
+
const v = VALIDATORS[museumCode];
|
|
107
|
+
if (!v) {
|
|
108
|
+
return reject(`unknown museum '${museumCode}': strict default reject`);
|
|
109
|
+
}
|
|
110
|
+
return v(raw);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=licenseGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"licenseGate.js","sourceRoot":"","sources":["../src/licenseGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function normalizeRegion(input: string | null | undefined): string | null;
|
|
2
|
+
export declare function detectAttributionType(artistName: string | null | undefined): 'named' | 'anonymous' | 'workshop' | 'after' | 'attributed' | 'circle' | 'follower';
|
|
3
|
+
export declare function cleanArtistName(input: string): string;
|
package/dist/mappings.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import regionsData from './data/regions.json' with { type: 'json' };
|
|
2
|
+
const regionMap = regionsData;
|
|
3
|
+
export function normalizeRegion(input) {
|
|
4
|
+
if (!input)
|
|
5
|
+
return null;
|
|
6
|
+
const lower = input.toLowerCase();
|
|
7
|
+
for (const [canonical, aliases] of Object.entries(regionMap)) {
|
|
8
|
+
if (aliases.some((alias) => lower.includes(alias))) {
|
|
9
|
+
return canonical;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const ATTRIBUTION_PATTERNS = [
|
|
15
|
+
{ test: /\bworkshop of\b/i, type: 'workshop' },
|
|
16
|
+
{ test: /\battributed to\b/i, type: 'attributed' },
|
|
17
|
+
{ test: /\bcircle of\b/i, type: 'circle' },
|
|
18
|
+
{ test: /\bfollower of\b/i, type: 'follower' },
|
|
19
|
+
{ test: /\bafter\b/i, type: 'after' },
|
|
20
|
+
{ test: /^(unknown|anonymous|unidentified)/i, type: 'anonymous' },
|
|
21
|
+
];
|
|
22
|
+
export function detectAttributionType(artistName) {
|
|
23
|
+
if (!artistName || !artistName.trim())
|
|
24
|
+
return 'anonymous';
|
|
25
|
+
for (const { test, type } of ATTRIBUTION_PATTERNS) {
|
|
26
|
+
if (test.test(artistName))
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
return 'named';
|
|
30
|
+
}
|
|
31
|
+
export function cleanArtistName(input) {
|
|
32
|
+
return input
|
|
33
|
+
.replace(/^(workshop of|attributed to|circle of|follower of|after)\s+/i, '')
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=mappings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mappings.js","sourceRoot":"","sources":["../src/mappings.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,qBAAqB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEpE,MAAM,SAAS,GAAG,WAAuC,CAAC;AAE1D,MAAM,UAAU,eAAe,CAAC,KAAgC;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnD,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,oBAAoB,GAGrB;IACH,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,UAAU,EAAE;IAC9C,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,YAAY,EAAE;IAClD,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC1C,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,UAAU,EAAE;IAC9C,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE;IACrC,EAAE,IAAI,EAAE,oCAAoC,EAAE,IAAI,EAAE,WAAW,EAAE;CAClE,CAAC;AAEF,MAAM,UAAU,qBAAqB,CACnC,UAAqC;IAErC,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE;QAAE,OAAO,WAAW,CAAC;IAC1D,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,oBAAoB,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;IACzC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,KAAK;SACT,OAAO,CAAC,8DAA8D,EAAE,EAAE,CAAC;SAC3E,IAAI,EAAE,CAAC;AACZ,CAAC"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { cite } from './cite.js';
|
|
9
|
+
import { Cache } from './db.js';
|
|
10
|
+
import { metFetcher } from './fetchers/met.js';
|
|
11
|
+
const FETCHERS = {
|
|
12
|
+
[metFetcher.code]: metFetcher,
|
|
13
|
+
};
|
|
14
|
+
const CACHE_PATH = process.env.OMM_CACHE_PATH ?? join(homedir(), '.open-museum-mcp', 'cache.db');
|
|
15
|
+
const cache = new Cache({ path: CACHE_PATH });
|
|
16
|
+
// Museum IDs are positive integers (no leading zeros). Tightening the regex
|
|
17
|
+
// here makes `met:000123` and `met:0` user errors rather than valid IDs that
|
|
18
|
+
// produce duplicate cache rows.
|
|
19
|
+
const ID_REGEX = /^[a-z]+:[1-9]\d*$/;
|
|
20
|
+
// Cap concurrent fetches to one museum's API. The Met has no batch endpoint,
|
|
21
|
+
// so a search of limit 50 fans out into up to 50 object fetches; without a
|
|
22
|
+
// cap, we'd hammer the upstream and risk rate-limit errors. 8 is empirically
|
|
23
|
+
// gentle and keeps wall-clock time within the same order of magnitude.
|
|
24
|
+
const FETCH_CONCURRENCY = 8;
|
|
25
|
+
async function withConcurrency(items, limit, fn) {
|
|
26
|
+
const results = new Array(items.length);
|
|
27
|
+
let next = 0;
|
|
28
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
29
|
+
while (true) {
|
|
30
|
+
const idx = next++;
|
|
31
|
+
if (idx >= items.length)
|
|
32
|
+
return;
|
|
33
|
+
results[idx] = await fn(items[idx]);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
await Promise.all(workers);
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
const SearchInput = z.object({
|
|
40
|
+
query: z.string().min(1),
|
|
41
|
+
museum: z.string().optional(),
|
|
42
|
+
has_image: z.boolean().default(true),
|
|
43
|
+
limit: z.number().int().min(1).max(50).default(10),
|
|
44
|
+
});
|
|
45
|
+
const GetInput = z.object({
|
|
46
|
+
id: z.string().regex(ID_REGEX),
|
|
47
|
+
});
|
|
48
|
+
const CiteInput = z.object({
|
|
49
|
+
id: z.string().regex(ID_REGEX),
|
|
50
|
+
style: z.enum(['short', 'full', 'caption']).default('full'),
|
|
51
|
+
});
|
|
52
|
+
async function fetchAndCache(id) {
|
|
53
|
+
if (!ID_REGEX.test(id)) {
|
|
54
|
+
return { ok: false, reason: `invalid artwork id: ${id}` };
|
|
55
|
+
}
|
|
56
|
+
const cached = cache.getObject(id);
|
|
57
|
+
if (cached)
|
|
58
|
+
return { ok: true, artwork: cached };
|
|
59
|
+
// ID_REGEX guarantees a non-empty `[a-z]+` segment before ':'.
|
|
60
|
+
const code = id.slice(0, id.indexOf(':'));
|
|
61
|
+
const fetcher = FETCHERS[code];
|
|
62
|
+
if (!fetcher)
|
|
63
|
+
return { ok: false, reason: `unknown museum code: ${code}` };
|
|
64
|
+
const raw = await fetcher.getRaw(id);
|
|
65
|
+
const result = fetcher.normalize(raw);
|
|
66
|
+
if (result.status === 'rejected') {
|
|
67
|
+
// Rejections are expected — strict-default-deny is the project's spine.
|
|
68
|
+
// Log to stderr (stdout is the MCP protocol channel) so operators can
|
|
69
|
+
// diagnose "why did my search return fewer results than expected?".
|
|
70
|
+
console.error(`[open-museum-mcp] rejected ${id}: ${result.rejection.reason}`);
|
|
71
|
+
return { ok: false, reason: result.rejection.reason };
|
|
72
|
+
}
|
|
73
|
+
cache.upsertObject(result.artwork);
|
|
74
|
+
return { ok: true, artwork: result.artwork };
|
|
75
|
+
}
|
|
76
|
+
const server = new Server({ name: 'open-museum-mcp', version: '0.1.0' }, {
|
|
77
|
+
capabilities: {
|
|
78
|
+
tools: {},
|
|
79
|
+
resources: {},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
83
|
+
tools: [
|
|
84
|
+
{
|
|
85
|
+
name: 'search_artworks',
|
|
86
|
+
description: 'Search across registered open-access museum collections. Returns artwork records that pass source-specific rights verification (ambiguous records excluded by default).',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
query: { type: 'string', description: 'Free-text query.' },
|
|
91
|
+
museum: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Optional museum code. Currently registered: met. (Cleveland, AIC adapters are planned for v0.2.)',
|
|
94
|
+
},
|
|
95
|
+
has_image: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
default: true,
|
|
98
|
+
description: 'Restrict to records with an image URL. Defaults to true. Note: some museums (e.g. The Met) only expose images-only search server-side.',
|
|
99
|
+
},
|
|
100
|
+
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
|
|
101
|
+
},
|
|
102
|
+
required: ['query'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'get_artwork',
|
|
107
|
+
description: 'Fetch a single artwork by its normalized ID (e.g. met:436533).',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
id: { type: 'string', description: 'Normalized artwork ID, format museumcode:numericid' },
|
|
112
|
+
},
|
|
113
|
+
required: ['id'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'cite',
|
|
118
|
+
description: 'Render a citation for an artwork. Styles: "full" (artist, title, date, museum, license, URL), "caption" (image caption form), "short" (inline reference).',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
id: { type: 'string', description: 'Normalized artwork ID.' },
|
|
123
|
+
style: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
enum: ['short', 'full', 'caption'],
|
|
126
|
+
default: 'full',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['id'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
}));
|
|
134
|
+
function errorResult(message) {
|
|
135
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
136
|
+
}
|
|
137
|
+
// The cache key includes the overfetch count, not the user-facing `limit`.
|
|
138
|
+
// That means limit:5 and limit:6 produce different keys (since 5*2=10 vs
|
|
139
|
+
// 6*2=12). The trade-off: more cache rows, but each row is guaranteed to
|
|
140
|
+
// hold enough IDs to satisfy a request at its overfetch tier even after
|
|
141
|
+
// rights-gate rejections. Bucketing would need explicit refill logic.
|
|
142
|
+
function searchCacheKey(query, museum, hasImage, overFetch) {
|
|
143
|
+
return JSON.stringify({ q: query, m: museum ?? '*', hi: hasImage, of: overFetch });
|
|
144
|
+
}
|
|
145
|
+
async function handleSearch(args) {
|
|
146
|
+
const input = SearchInput.parse(args);
|
|
147
|
+
const fetchers = input.museum
|
|
148
|
+
? (FETCHERS[input.museum] ? [FETCHERS[input.museum]] : [])
|
|
149
|
+
: Object.values(FETCHERS);
|
|
150
|
+
if (fetchers.length === 0) {
|
|
151
|
+
return errorResult(`unknown museum: ${input.museum}`);
|
|
152
|
+
}
|
|
153
|
+
const overFetch = input.has_image ? input.limit * 2 : input.limit;
|
|
154
|
+
const cacheKey = searchCacheKey(input.query, input.museum, input.has_image, overFetch);
|
|
155
|
+
let allIds = cache.getQuery(cacheKey);
|
|
156
|
+
if (!allIds) {
|
|
157
|
+
const idLists = await Promise.all(fetchers.map((f) => f.search(input.query, overFetch, { hasImage: input.has_image }).catch(() => [])));
|
|
158
|
+
allIds = idLists.flat();
|
|
159
|
+
cache.putQuery(cacheKey, allIds);
|
|
160
|
+
}
|
|
161
|
+
const fetched = await withConcurrency(allIds, FETCH_CONCURRENCY, (id) => fetchAndCache(id).catch((err) => ({
|
|
162
|
+
ok: false,
|
|
163
|
+
reason: err instanceof Error ? err.message : 'fetch failed',
|
|
164
|
+
})));
|
|
165
|
+
const accepted = fetched
|
|
166
|
+
.filter((r) => r.ok)
|
|
167
|
+
.map((r) => r.artwork);
|
|
168
|
+
const filtered = accepted.filter((a) => !input.has_image || Boolean(a.imageUrls.full));
|
|
169
|
+
const results = filtered.slice(0, input.limit);
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: JSON.stringify({ count: results.length, results }, null, 2),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function handleGet(args) {
|
|
180
|
+
const input = GetInput.parse(args);
|
|
181
|
+
const out = await fetchAndCache(input.id);
|
|
182
|
+
if (!out.ok)
|
|
183
|
+
return errorResult(out.reason);
|
|
184
|
+
return { content: [{ type: 'text', text: JSON.stringify(out.artwork, null, 2) }] };
|
|
185
|
+
}
|
|
186
|
+
async function handleCite(args) {
|
|
187
|
+
const input = CiteInput.parse(args);
|
|
188
|
+
const out = await fetchAndCache(input.id);
|
|
189
|
+
if (!out.ok)
|
|
190
|
+
return errorResult(out.reason);
|
|
191
|
+
return { content: [{ type: 'text', text: cite(out.artwork, input.style) }] };
|
|
192
|
+
}
|
|
193
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
194
|
+
const { name, arguments: args } = request.params;
|
|
195
|
+
try {
|
|
196
|
+
if (name === 'search_artworks')
|
|
197
|
+
return await handleSearch(args);
|
|
198
|
+
if (name === 'get_artwork')
|
|
199
|
+
return await handleGet(args);
|
|
200
|
+
if (name === 'cite')
|
|
201
|
+
return await handleCite(args);
|
|
202
|
+
return errorResult(`unknown tool: ${name}`);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (err instanceof z.ZodError) {
|
|
206
|
+
return errorResult(`invalid input: ${err.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
|
|
207
|
+
}
|
|
208
|
+
if (err instanceof Error) {
|
|
209
|
+
return errorResult(`${name} failed: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
return errorResult(`${name} failed with unknown error`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
215
|
+
resources: [],
|
|
216
|
+
}));
|
|
217
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
218
|
+
const uri = request.params.uri;
|
|
219
|
+
const m = uri.match(/^museum:\/\/([a-z]+)\/(.+)$/);
|
|
220
|
+
if (!m) {
|
|
221
|
+
throw new Error(`invalid museum URI scheme: ${uri}`);
|
|
222
|
+
}
|
|
223
|
+
const id = `${m[1]}:${m[2]}`;
|
|
224
|
+
if (!ID_REGEX.test(id)) {
|
|
225
|
+
throw new Error(`invalid museum URI: ${uri} (id must match ${ID_REGEX})`);
|
|
226
|
+
}
|
|
227
|
+
const out = await fetchAndCache(id);
|
|
228
|
+
if (!out.ok) {
|
|
229
|
+
throw new Error(`cannot resolve ${uri}: ${out.reason}`);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
contents: [
|
|
233
|
+
{
|
|
234
|
+
uri,
|
|
235
|
+
mimeType: 'application/json',
|
|
236
|
+
text: JSON.stringify(out.artwork, null, 2),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const transport = new StdioServerTransport();
|
|
242
|
+
await server.connect(transport);
|
|
243
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type LicenseType = 'CC0' | 'PD' | 'CC-BY' | 'CC-BY-SA' | 'OTHER' | 'UNKNOWN';
|
|
2
|
+
export type AttributionType = 'named' | 'anonymous' | 'workshop' | 'after' | 'attributed' | 'circle' | 'follower';
|
|
3
|
+
export interface Artist {
|
|
4
|
+
name: string;
|
|
5
|
+
nationality?: string;
|
|
6
|
+
lifespan?: string;
|
|
7
|
+
attributionType: AttributionType;
|
|
8
|
+
}
|
|
9
|
+
export type RightsConfidence = 'high' | 'medium' | 'low';
|
|
10
|
+
export interface ArtworkLicense {
|
|
11
|
+
type: LicenseType;
|
|
12
|
+
rawValue: string;
|
|
13
|
+
verificationSource: string;
|
|
14
|
+
verifiedAt: string;
|
|
15
|
+
confidence: RightsConfidence;
|
|
16
|
+
}
|
|
17
|
+
export interface ArtworkImages {
|
|
18
|
+
full: string;
|
|
19
|
+
large?: string;
|
|
20
|
+
thumbnail?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ArtworkSource {
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
pageUrl: string;
|
|
25
|
+
}
|
|
26
|
+
export interface MuseumRef {
|
|
27
|
+
code: string;
|
|
28
|
+
name: string;
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
export interface Artwork {
|
|
32
|
+
id: string;
|
|
33
|
+
museum: MuseumRef;
|
|
34
|
+
title: string;
|
|
35
|
+
artist: Artist;
|
|
36
|
+
displayDate: string;
|
|
37
|
+
yearStart: number | null;
|
|
38
|
+
yearEnd: number | null;
|
|
39
|
+
medium: string;
|
|
40
|
+
region: string | null;
|
|
41
|
+
period: string | null;
|
|
42
|
+
imageUrls: ArtworkImages;
|
|
43
|
+
imageOpenAccess: boolean;
|
|
44
|
+
metadataOpenAccess: boolean;
|
|
45
|
+
license: ArtworkLicense;
|
|
46
|
+
source: ArtworkSource;
|
|
47
|
+
description?: string;
|
|
48
|
+
rawTags?: string[];
|
|
49
|
+
/** Reserved for v1.0 artist-obscurity scoring across the federated corpus. Not yet populated by any fetcher. */
|
|
50
|
+
obscurityScore?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `rawSnapshot` preserves the museum's verbatim response when a record is
|
|
54
|
+
* rejected by the rights gate. It exists for two purposes only:
|
|
55
|
+
* 1. Debugging when a museum quietly changes a field name; the snapshot
|
|
56
|
+
* shows what they actually returned.
|
|
57
|
+
* 2. Authoring rejection fixtures by copying a real-world rejection into
|
|
58
|
+
* `tests/fixtures/`.
|
|
59
|
+
* Never expose this on the wire to MCP clients — it can include arbitrary
|
|
60
|
+
* museum payload contents that aren't meant for end users.
|
|
61
|
+
*/
|
|
62
|
+
export interface RejectedArtwork {
|
|
63
|
+
id: string;
|
|
64
|
+
museumCode: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
rawSnapshot: unknown;
|
|
67
|
+
}
|
|
68
|
+
export type ValidationResult = {
|
|
69
|
+
status: 'accepted';
|
|
70
|
+
artwork: Artwork;
|
|
71
|
+
} | {
|
|
72
|
+
status: 'rejected';
|
|
73
|
+
rejection: RejectedArtwork;
|
|
74
|
+
};
|
|
75
|
+
export interface DateRange {
|
|
76
|
+
yearStart: number | null;
|
|
77
|
+
yearEnd: number | null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reserved for v0.2 list_traditions tool: each normalized tradition tag will
|
|
81
|
+
* carry per-museum coverage counts so callers can see where holdings live
|
|
82
|
+
* before searching. Not yet emitted by any tool.
|
|
83
|
+
*/
|
|
84
|
+
export interface Tradition {
|
|
85
|
+
tag: string;
|
|
86
|
+
label: string;
|
|
87
|
+
coverage: Record<string, number>;
|
|
88
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open-museum-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for license-verified search across open-access museum collections (The Met, Cleveland, AIC and more).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"open-museum-mcp": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/cfpramod/open-museum-mcp.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/cfpramod/open-museum-mcp/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/cfpramod/open-museum-mcp#readme",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsx src/server.ts",
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"museum",
|
|
35
|
+
"open-access",
|
|
36
|
+
"art",
|
|
37
|
+
"metropolitan-museum",
|
|
38
|
+
"cleveland-museum",
|
|
39
|
+
"art-institute-chicago"
|
|
40
|
+
],
|
|
41
|
+
"author": "Pramod Prasanth",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
48
|
+
"better-sqlite3": "^11.5.0",
|
|
49
|
+
"zod": "^3.23.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
53
|
+
"@types/node": "^22.10.0",
|
|
54
|
+
"tsx": "^4.19.2",
|
|
55
|
+
"typescript": "^5.7.2",
|
|
56
|
+
"vitest": "^4.1.5"
|
|
57
|
+
}
|
|
58
|
+
}
|