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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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;
@@ -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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }