@mediagraph/mcp 1.0.11 → 1.0.13

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/dist/index.js CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
13
  import { exec } from "child_process";
14
14
  import { platform } from "os";
15
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
16
+ import { fileURLToPath } from "url";
17
+ import { dirname as dirname2, join as join2 } from "path";
15
18
 
16
19
  // src/auth/oauth.ts
17
20
  import { createHash, randomBytes } from "crypto";
@@ -660,13 +663,32 @@ var MediagraphClient = class {
660
663
  return this.request("GET", `/api/assets/${id}/face_taggings`);
661
664
  }
662
665
  async getAssetDownload(id, options) {
663
- const params = {};
664
- if (options?.size) params.size = options.size;
665
- if (options?.watermarked) params.watermarked = true;
666
- if (options?.version_number) params.version_number = options.version_number;
667
- if (options?.via) params.via = options.via;
668
- if (options?.skip_meta) params.skip_meta = true;
669
- return this.request("GET", `/api/assets/${id}/download`, { params });
666
+ const prepareResponse = await this.createDownload({
667
+ asset_ids: [typeof id === "string" ? parseInt(id, 10) : id],
668
+ size: options?.size || "original",
669
+ watermarked: options?.watermarked,
670
+ via: options?.via,
671
+ skip_meta: options?.skip_meta
672
+ });
673
+ const downloadUrl = `${this.apiUrl}/api/downloads/${prepareResponse.token}`;
674
+ return {
675
+ url: downloadUrl,
676
+ filename: prepareResponse.filename || `asset-${id}`
677
+ };
678
+ }
679
+ async getBulkDownload(options) {
680
+ const prepareResponse = await this.createDownload({
681
+ asset_ids: options.asset_ids,
682
+ size: options.size || "original",
683
+ watermarked: options.watermarked,
684
+ via: options.via,
685
+ skip_meta: options.skip_meta
686
+ });
687
+ const downloadUrl = `${this.apiUrl}/api/downloads/${prepareResponse.token}`;
688
+ return {
689
+ url: downloadUrl,
690
+ filename: prepareResponse.filename || `mediagraph-download-${options.asset_ids.length}-assets.zip`
691
+ };
670
692
  }
671
693
  async addAssetVersion(id, data) {
672
694
  return this.request("POST", `/api/assets/${id}/add_version`, { body: data });
@@ -1247,7 +1269,20 @@ var MediagraphClient = class {
1247
1269
  return this.request("GET", `/api/downloads/${token}`);
1248
1270
  }
1249
1271
  async createDownload(data) {
1250
- return this.request("POST", "/api/downloads", { body: { download: data } });
1272
+ const download = {
1273
+ asset_ids: data.asset_ids,
1274
+ size: data.size
1275
+ };
1276
+ if (data.watermarked === true) {
1277
+ download.watermarked = true;
1278
+ }
1279
+ if (data.via) {
1280
+ download.via = data.via;
1281
+ }
1282
+ if (data.skip_meta === true) {
1283
+ download.skip_meta = true;
1284
+ }
1285
+ return this.request("POST", "/api/downloads", { body: { download } });
1251
1286
  }
1252
1287
  // ============================================================================
1253
1288
  // Webhooks
@@ -1649,6 +1684,29 @@ COMMON SEARCH FIELDS:
1649
1684
  required: ["id"]
1650
1685
  }
1651
1686
  },
1687
+ {
1688
+ name: "bulk_download_assets",
1689
+ description: "Get a download URL for multiple assets (returns a ZIP file)",
1690
+ inputSchema: {
1691
+ type: "object",
1692
+ properties: {
1693
+ asset_ids: {
1694
+ type: "array",
1695
+ items: { type: "number" },
1696
+ description: "Array of asset IDs to download"
1697
+ },
1698
+ size: {
1699
+ type: "string",
1700
+ enum: ["small", "permalink", "full", "original"],
1701
+ description: "Maximum size for all assets in the download (default: original)"
1702
+ },
1703
+ watermarked: { type: "boolean", description: "Request watermarked versions" },
1704
+ via: { type: "string", description: "Description of the app or integration making the call" },
1705
+ skip_meta: { type: "boolean", description: "Do not write metadata to files" }
1706
+ },
1707
+ required: ["asset_ids"]
1708
+ }
1709
+ },
1652
1710
  {
1653
1711
  name: "get_asset_auto_tags",
1654
1712
  description: "Get AI-generated auto tags for an asset",
@@ -1728,6 +1786,15 @@ COMMON SEARCH FIELDS:
1728
1786
  version_number: args.version_number
1729
1787
  }));
1730
1788
  },
1789
+ async bulk_download_assets(args, { client: client2 }) {
1790
+ return successResult(await client2.getBulkDownload({
1791
+ asset_ids: args.asset_ids,
1792
+ size: args.size,
1793
+ watermarked: args.watermarked,
1794
+ via: args.via,
1795
+ skip_meta: args.skip_meta
1796
+ }));
1797
+ },
1731
1798
  async get_asset_auto_tags(args, { client: client2 }) {
1732
1799
  return successResult(await client2.getAssetAutoTags(args.id));
1733
1800
  },
@@ -2647,8 +2714,8 @@ var downloadTools = {
2647
2714
  asset_ids: { type: "array", items: { type: "number" } },
2648
2715
  size: {
2649
2716
  type: "string",
2650
- enum: ["small", "small-watermark", "permalink", "permalink-watermark", "full", "full-watermark", "original"],
2651
- description: "Maximum size requested for assets in the download"
2717
+ enum: ["small", "medium", "full", "original"],
2718
+ description: "Maximum size requested for assets in the download (default: original)"
2652
2719
  }
2653
2720
  },
2654
2721
  required: ["asset_ids"]
@@ -2662,7 +2729,8 @@ var downloadTools = {
2662
2729
  ],
2663
2730
  handlers: {
2664
2731
  async create_download(args, { client: client2 }) {
2665
- return successResult(await client2.createDownload(args));
2732
+ const { asset_ids, size = "original" } = args;
2733
+ return successResult(await client2.createDownload({ asset_ids, size }));
2666
2734
  },
2667
2735
  async get_download(args, { client: client2 }) {
2668
2736
  return successResult(await client2.getDownload(args.token));
@@ -3281,6 +3349,100 @@ var adminTools = {
3281
3349
  }
3282
3350
  };
3283
3351
 
3352
+ // src/tools/app.ts
3353
+ var appTools = {
3354
+ definitions: [
3355
+ {
3356
+ name: "search_assets_visual",
3357
+ description: `Search for assets and display results in an interactive visual gallery.
3358
+
3359
+ This tool provides a rich visual interface for browsing search results with:
3360
+ - Thumbnail grid with hover previews
3361
+ - Click to view full asset details
3362
+ - Inline editing of metadata
3363
+ - Rating, tagging, and download options
3364
+ - Pagination controls
3365
+
3366
+ Use this when the user wants to visually browse or explore assets.`,
3367
+ inputSchema: {
3368
+ type: "object",
3369
+ properties: {
3370
+ q: { type: "string", description: "Search query with optional advanced operators (AND, OR, NOT, field:value, wildcards)" },
3371
+ ...paginationParams,
3372
+ ids: { type: "array", items: { type: "number" }, description: "Filter by specific asset IDs" },
3373
+ guids: { type: "array", items: { type: "string" }, description: "Filter by specific asset GUIDs" },
3374
+ tags: { type: "array", items: { type: "string" }, description: "Filter by tags" },
3375
+ collection_id: { type: "number", description: "Filter by collection ID" },
3376
+ storage_folder_id: { type: "number", description: "Filter by storage folder ID" },
3377
+ lightbox_id: { type: "number", description: "Filter by lightbox ID" },
3378
+ exts: { type: "array", items: { type: "string" }, description: "Filter by file extensions" },
3379
+ rating: { type: "array", items: { type: "number" }, description: "Filter by rating range [min, max]" },
3380
+ aspect: { type: "string", enum: ["square", "portrait", "landscape", "panorama"] },
3381
+ has_people: { type: "string", enum: ["yes", "no", "untagged"] },
3382
+ has_alt_text: { type: "string", enum: ["yes", "no"] },
3383
+ gps: { type: "boolean", description: "Filter for assets with GPS data" },
3384
+ captured_at: { type: "array", items: { type: "string" }, description: "Date range [start, end] in ISO 8601" },
3385
+ created_at: { type: "array", items: { type: "string" }, description: "Date range [start, end] in ISO 8601" }
3386
+ },
3387
+ required: []
3388
+ },
3389
+ // MCP Apps metadata - tells the host to display the UI
3390
+ // Include both formats for compatibility with different host versions
3391
+ _meta: {
3392
+ ui: {
3393
+ resourceUri: "ui://mediagraph/gallery"
3394
+ },
3395
+ "ui/resourceUri": "ui://mediagraph/gallery"
3396
+ // Legacy format for older hosts
3397
+ }
3398
+ }
3399
+ ],
3400
+ handlers: {
3401
+ async search_assets_visual(args, { client: client2, organizationSlug }) {
3402
+ const params = {
3403
+ ...args,
3404
+ include_renditions: false,
3405
+ // Don't include all renditions to reduce size
3406
+ include_totals: true,
3407
+ per_page: args.per_page || 24
3408
+ // Reasonable default
3409
+ };
3410
+ const response = await client2.searchAssets(params);
3411
+ const lightAssets = response.assets.map((asset) => ({
3412
+ id: asset.id,
3413
+ guid: asset.guid,
3414
+ filename: asset.filename,
3415
+ title: asset.title,
3416
+ description: asset.description,
3417
+ alt_text: asset.alt_text,
3418
+ type: asset.type || asset.file_type,
3419
+ ext: asset.ext,
3420
+ width: asset.width,
3421
+ height: asset.height,
3422
+ duration: asset.duration,
3423
+ rating: asset.rating,
3424
+ // Tags come as objects from API, extract just the names
3425
+ tags: asset.tags?.map((tag) => typeof tag === "string" ? tag : tag.name).filter(Boolean),
3426
+ thumb_url: asset.thumb_url,
3427
+ grid_url: asset.grid_url,
3428
+ small_url: asset.small_url,
3429
+ preview_url: asset.preview_url,
3430
+ created_at: asset.created_at,
3431
+ updated_at: asset.updated_at,
3432
+ captured_at: asset.captured_at
3433
+ }));
3434
+ return successResult({
3435
+ assets: lightAssets,
3436
+ total: response.total,
3437
+ page: response.page,
3438
+ per_page: response.per_page,
3439
+ total_pages: response.total_pages,
3440
+ organization_slug: organizationSlug
3441
+ });
3442
+ }
3443
+ }
3444
+ };
3445
+
3284
3446
  // src/tools/index.ts
3285
3447
  var allToolModules = [
3286
3448
  userTools,
@@ -3296,7 +3458,8 @@ var allToolModules = [
3296
3458
  downloadTools,
3297
3459
  uploadTools,
3298
3460
  webhookTools,
3299
- adminTools
3461
+ adminTools,
3462
+ appTools
3300
3463
  ];
3301
3464
  var toolDefinitions = allToolModules.flatMap((m) => m.definitions);
3302
3465
  var allHandlers = {};
@@ -3495,31 +3658,56 @@ function openBrowser(url) {
3495
3658
  }
3496
3659
  async function runAutoAuth() {
3497
3660
  if (isAuthInProgress) {
3498
- while (isAuthInProgress) {
3499
- await new Promise((resolve) => setTimeout(resolve, 100));
3661
+ console.error("[MCP] OAuth already in progress, waiting for completion...");
3662
+ const waitStart = Date.now();
3663
+ const maxWait = 12e4;
3664
+ while (isAuthInProgress && Date.now() - waitStart < maxWait) {
3665
+ await new Promise((resolve) => setTimeout(resolve, 500));
3666
+ }
3667
+ if (isAuthInProgress) {
3668
+ console.error("[MCP] Timed out waiting for existing OAuth flow");
3669
+ return false;
3500
3670
  }
3501
3671
  return currentTokens !== null;
3502
3672
  }
3503
3673
  isAuthInProgress = true;
3674
+ console.error("[MCP] Starting OAuth flow...");
3504
3675
  try {
3505
3676
  const authUrl = oauthHandler.getAuthorizationUrl();
3506
3677
  await oauthHandler.startCallbackServer();
3678
+ console.error("[MCP] Callback server ready, opening browser...");
3507
3679
  openBrowser(authUrl);
3680
+ console.error("[MCP] Waiting for OAuth callback...");
3508
3681
  const { code } = await oauthHandler.waitForCallback();
3682
+ console.error("[MCP] OAuth callback received, exchanging code...");
3509
3683
  const tokens = await oauthHandler.exchangeCode(code);
3510
3684
  currentTokens = tokens;
3511
- const whoami = await client.whoami();
3512
- const storedData = {
3513
- tokens,
3514
- organizationId: whoami.organization.id,
3515
- organizationName: whoami.organization.name,
3516
- userId: whoami.user.id,
3517
- userEmail: whoami.user.email
3518
- };
3685
+ console.error("[MCP] Token exchange successful");
3686
+ let storedData = { tokens };
3519
3687
  tokenStore.save(storedData);
3688
+ try {
3689
+ const whoami = await client.whoami();
3690
+ if (whoami?.organization?.id) {
3691
+ const org = whoami.organization;
3692
+ storedData = {
3693
+ tokens,
3694
+ organizationId: org.id,
3695
+ organizationName: org.title || org.name,
3696
+ organizationSlug: org.slug,
3697
+ userId: whoami.user?.id,
3698
+ userEmail: whoami.user?.email
3699
+ };
3700
+ tokenStore.save(storedData);
3701
+ console.error(`[MCP] Authenticated as ${whoami.user?.email} in ${org.title || org.name}`);
3702
+ } else {
3703
+ console.error("[MCP] Authenticated (whoami returned incomplete data)");
3704
+ }
3705
+ } catch (whoamiError) {
3706
+ console.error("[MCP] Authenticated (whoami failed, tokens saved):", whoamiError);
3707
+ }
3520
3708
  return true;
3521
3709
  } catch (error) {
3522
- console.error("Auto-auth failed:", error);
3710
+ console.error("[MCP] Auto-auth failed:", error);
3523
3711
  oauthHandler.stopCallbackServer();
3524
3712
  return false;
3525
3713
  } finally {
@@ -3558,6 +3746,10 @@ var client = new MediagraphClient({
3558
3746
  });
3559
3747
  var toolContext = { client };
3560
3748
  var resourceContext = { client };
3749
+ function getOrganizationSlug() {
3750
+ const stored = tokenStore.load();
3751
+ return stored?.organizationSlug;
3752
+ }
3561
3753
  var server = new Server(
3562
3754
  {
3563
3755
  name: "mediagraph-mcp",
@@ -3579,33 +3771,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3579
3771
  const { name, arguments: args } = request.params;
3580
3772
  let token = await getAccessToken();
3581
3773
  if (!token) {
3582
- const authSuccess = await runAutoAuth();
3583
- if (!authSuccess) {
3584
- return {
3585
- content: [
3586
- {
3587
- type: "text",
3588
- text: "Failed to authenticate with Mediagraph. Please try again or check your browser for the authorization window."
3589
- }
3590
- ],
3591
- isError: true
3592
- };
3774
+ if (isAuthInProgress) {
3775
+ console.error("[MCP] OAuth in progress, waiting...");
3776
+ const waitStart = Date.now();
3777
+ const maxWait = 12e4;
3778
+ while (isAuthInProgress && Date.now() - waitStart < maxWait) {
3779
+ await new Promise((resolve) => setTimeout(resolve, 500));
3780
+ }
3781
+ token = await getAccessToken();
3782
+ if (token) {
3783
+ console.error("[MCP] OAuth completed, proceeding with request");
3784
+ }
3593
3785
  }
3594
- token = await getAccessToken();
3595
3786
  if (!token) {
3596
- return {
3597
- content: [
3598
- {
3599
- type: "text",
3600
- text: "Authentication completed but failed to retrieve access token. Please try again."
3601
- }
3602
- ],
3603
- isError: true
3604
- };
3787
+ const authSuccess = await runAutoAuth();
3788
+ if (!authSuccess) {
3789
+ return {
3790
+ content: [
3791
+ {
3792
+ type: "text",
3793
+ text: "Authorization is in progress. Please complete the login in your browser, then try this request again."
3794
+ }
3795
+ ],
3796
+ isError: true
3797
+ };
3798
+ }
3799
+ token = await getAccessToken();
3800
+ if (!token) {
3801
+ return {
3802
+ content: [
3803
+ {
3804
+ type: "text",
3805
+ text: "Authentication completed but failed to retrieve access token. Please try again."
3806
+ }
3807
+ ],
3808
+ isError: true
3809
+ };
3810
+ }
3605
3811
  }
3606
3812
  }
3607
3813
  console.error(`[MCP] Tool call: ${name}`);
3608
3814
  console.error(`[MCP] Arguments: ${JSON.stringify(args, null, 2)}`);
3815
+ toolContext.organizationSlug = getOrganizationSlug();
3609
3816
  const result = await handleTool(name, args || {}, toolContext);
3610
3817
  if (result.isError) {
3611
3818
  console.error(`[MCP] Tool error: ${result.content[0]?.text}`);
@@ -3632,6 +3839,9 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
3632
3839
  });
3633
3840
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3634
3841
  const { uri } = request.params;
3842
+ if (uri.startsWith("ui://mediagraph/")) {
3843
+ return handleAppResource(uri);
3844
+ }
3635
3845
  let token = await getAccessToken();
3636
3846
  if (!token) {
3637
3847
  const authSuccess = await runAutoAuth();
@@ -3664,6 +3874,57 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3664
3874
  contents: [content]
3665
3875
  };
3666
3876
  });
3877
+ function handleAppResource(uri) {
3878
+ const __filename = fileURLToPath(import.meta.url);
3879
+ const __dirname = dirname2(__filename);
3880
+ const appPath = join2(__dirname, "app", "index.html");
3881
+ if (!existsSync2(appPath)) {
3882
+ console.error(`[MCP] App resource not found at: ${appPath}`);
3883
+ return {
3884
+ contents: [
3885
+ {
3886
+ uri,
3887
+ mimeType: "text/plain",
3888
+ text: "MCP App UI not found. Please rebuild the project with npm run build."
3889
+ }
3890
+ ]
3891
+ };
3892
+ }
3893
+ try {
3894
+ const html = readFileSync2(appPath, "utf-8");
3895
+ return {
3896
+ contents: [
3897
+ {
3898
+ uri,
3899
+ mimeType: "text/html;profile=mcp-app",
3900
+ text: html,
3901
+ // CSP configuration to allow loading images from Mediagraph CDN
3902
+ _meta: {
3903
+ ui: {
3904
+ csp: {
3905
+ // Allow images from CloudFront CDN
3906
+ resourceDomains: ["https://*.cloudfront.net"],
3907
+ // Allow API calls to Mediagraph (for future use)
3908
+ connectDomains: ["https://api.mediagraph.io"]
3909
+ }
3910
+ }
3911
+ }
3912
+ }
3913
+ ]
3914
+ };
3915
+ } catch (error) {
3916
+ console.error("[MCP] Failed to read app resource:", error);
3917
+ return {
3918
+ contents: [
3919
+ {
3920
+ uri,
3921
+ mimeType: "text/plain",
3922
+ text: "Failed to load MCP App UI."
3923
+ }
3924
+ ]
3925
+ };
3926
+ }
3927
+ }
3667
3928
  async function runAuthorize() {
3668
3929
  if (!config.clientId) {
3669
3930
  console.error("Error: MEDIAGRAPH_CLIENT_ID environment variable is required");
@@ -3690,18 +3951,25 @@ async function runAuthorize() {
3690
3951
  console.log("Tokens received successfully.");
3691
3952
  currentTokens = tokens;
3692
3953
  const whoami = await client.whoami();
3954
+ console.log("Whoami response:", JSON.stringify(whoami, null, 2));
3955
+ const org = whoami.organization;
3693
3956
  const storedData = {
3694
3957
  tokens,
3695
- organizationId: whoami.organization.id,
3696
- organizationName: whoami.organization.name,
3697
- userId: whoami.user.id,
3698
- userEmail: whoami.user.email
3958
+ organizationId: org?.id,
3959
+ organizationName: org?.title || org?.name,
3960
+ organizationSlug: org?.slug,
3961
+ userId: whoami.user?.id,
3962
+ userEmail: whoami.user?.email
3699
3963
  };
3700
3964
  tokenStore.save(storedData);
3701
3965
  console.log("");
3702
3966
  console.log("Successfully authorized!");
3703
- console.log(`Organization: ${whoami.organization.name}`);
3704
- console.log(`User: ${whoami.user.full_name} (${whoami.user.email})`);
3967
+ if (org) {
3968
+ console.log(`Organization: ${org.title || org.name} (${org.slug})`);
3969
+ }
3970
+ if (whoami.user) {
3971
+ console.log(`User: ${whoami.user.full_name || whoami.user.email} (${whoami.user.email})`);
3972
+ }
3705
3973
  console.log("");
3706
3974
  console.log("You can now use the Mediagraph MCP server.");
3707
3975
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediagraph/mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "MCP server for Mediagraph - Media Asset Management Platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,10 +8,15 @@
8
8
  "mediagraph-mcp": "./dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "build": "NODE_ENV=production tsup src/index.ts --format esm --dts --clean && npm run build:app:prod",
12
+ "build:dev": "tsup src/index.ts --format esm --dts --clean && npm run build:app:dev",
13
+ "build:app:prod": "NODE_ENV=production vite build --config vite.config.ts",
14
+ "build:app:dev": "NODE_ENV=development vite build --config vite.config.ts",
15
+ "build:app": "vite build --config vite.config.ts",
12
16
  "dev": "tsup src/index.ts --format esm --watch",
17
+ "dev:app": "vite --config vite.config.ts",
13
18
  "start": "node dist/index.js",
14
- "typecheck": "tsc --noEmit",
19
+ "typecheck": "tsc --noEmit && tsc --project tsconfig.app.json --noEmit",
15
20
  "lint": "eslint src/**/*.ts",
16
21
  "test": "vitest run",
17
22
  "test:watch": "vitest",
@@ -46,13 +51,21 @@
46
51
  "LICENSE"
47
52
  ],
48
53
  "dependencies": {
54
+ "@modelcontextprotocol/ext-apps": "^1.0.1",
49
55
  "@modelcontextprotocol/sdk": "^1.0.0"
50
56
  },
51
57
  "devDependencies": {
52
58
  "@types/node": "^20.10.0",
59
+ "@types/react": "^18.3.0",
60
+ "@types/react-dom": "^18.3.0",
61
+ "@vitejs/plugin-react": "^4.3.0",
53
62
  "eslint": "^8.56.0",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
54
65
  "tsup": "^8.0.1",
55
66
  "typescript": "^5.3.3",
67
+ "vite": "^5.4.0",
68
+ "vite-plugin-singlefile": "^2.0.0",
56
69
  "vitest": "^1.1.0"
57
70
  }
58
71
  }