@mediagraph/mcp 1.0.10 → 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 });
@@ -1218,8 +1240,7 @@ var MediagraphClient = class {
1218
1240
  return this.request("GET", `/api/comments/${id}`);
1219
1241
  }
1220
1242
  async createComment(type, id, data) {
1221
- return this.request("POST", "/api/comments", {
1222
- params: { type, id },
1243
+ return this.request("POST", `/api/comments?type=${encodeURIComponent(type)}&id=${id}`, {
1223
1244
  body: { comment: data }
1224
1245
  });
1225
1246
  }
@@ -1248,7 +1269,20 @@ var MediagraphClient = class {
1248
1269
  return this.request("GET", `/api/downloads/${token}`);
1249
1270
  }
1250
1271
  async createDownload(data) {
1251
- 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 } });
1252
1286
  }
1253
1287
  // ============================================================================
1254
1288
  // Webhooks
@@ -1650,6 +1684,29 @@ COMMON SEARCH FIELDS:
1650
1684
  required: ["id"]
1651
1685
  }
1652
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
+ },
1653
1710
  {
1654
1711
  name: "get_asset_auto_tags",
1655
1712
  description: "Get AI-generated auto tags for an asset",
@@ -1729,6 +1786,15 @@ COMMON SEARCH FIELDS:
1729
1786
  version_number: args.version_number
1730
1787
  }));
1731
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
+ },
1732
1798
  async get_asset_auto_tags(args, { client: client2 }) {
1733
1799
  return successResult(await client2.getAssetAutoTags(args.id));
1734
1800
  },
@@ -2648,8 +2714,8 @@ var downloadTools = {
2648
2714
  asset_ids: { type: "array", items: { type: "number" } },
2649
2715
  size: {
2650
2716
  type: "string",
2651
- enum: ["small", "small-watermark", "permalink", "permalink-watermark", "full", "full-watermark", "original"],
2652
- 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)"
2653
2719
  }
2654
2720
  },
2655
2721
  required: ["asset_ids"]
@@ -2663,7 +2729,8 @@ var downloadTools = {
2663
2729
  ],
2664
2730
  handlers: {
2665
2731
  async create_download(args, { client: client2 }) {
2666
- return successResult(await client2.createDownload(args));
2732
+ const { asset_ids, size = "original" } = args;
2733
+ return successResult(await client2.createDownload({ asset_ids, size }));
2667
2734
  },
2668
2735
  async get_download(args, { client: client2 }) {
2669
2736
  return successResult(await client2.getDownload(args.token));
@@ -3282,6 +3349,100 @@ var adminTools = {
3282
3349
  }
3283
3350
  };
3284
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
+
3285
3446
  // src/tools/index.ts
3286
3447
  var allToolModules = [
3287
3448
  userTools,
@@ -3297,7 +3458,8 @@ var allToolModules = [
3297
3458
  downloadTools,
3298
3459
  uploadTools,
3299
3460
  webhookTools,
3300
- adminTools
3461
+ adminTools,
3462
+ appTools
3301
3463
  ];
3302
3464
  var toolDefinitions = allToolModules.flatMap((m) => m.definitions);
3303
3465
  var allHandlers = {};
@@ -3496,31 +3658,56 @@ function openBrowser(url) {
3496
3658
  }
3497
3659
  async function runAutoAuth() {
3498
3660
  if (isAuthInProgress) {
3499
- while (isAuthInProgress) {
3500
- 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;
3501
3670
  }
3502
3671
  return currentTokens !== null;
3503
3672
  }
3504
3673
  isAuthInProgress = true;
3674
+ console.error("[MCP] Starting OAuth flow...");
3505
3675
  try {
3506
3676
  const authUrl = oauthHandler.getAuthorizationUrl();
3507
3677
  await oauthHandler.startCallbackServer();
3678
+ console.error("[MCP] Callback server ready, opening browser...");
3508
3679
  openBrowser(authUrl);
3680
+ console.error("[MCP] Waiting for OAuth callback...");
3509
3681
  const { code } = await oauthHandler.waitForCallback();
3682
+ console.error("[MCP] OAuth callback received, exchanging code...");
3510
3683
  const tokens = await oauthHandler.exchangeCode(code);
3511
3684
  currentTokens = tokens;
3512
- const whoami = await client.whoami();
3513
- const storedData = {
3514
- tokens,
3515
- organizationId: whoami.organization.id,
3516
- organizationName: whoami.organization.name,
3517
- userId: whoami.user.id,
3518
- userEmail: whoami.user.email
3519
- };
3685
+ console.error("[MCP] Token exchange successful");
3686
+ let storedData = { tokens };
3520
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
+ }
3521
3708
  return true;
3522
3709
  } catch (error) {
3523
- console.error("Auto-auth failed:", error);
3710
+ console.error("[MCP] Auto-auth failed:", error);
3524
3711
  oauthHandler.stopCallbackServer();
3525
3712
  return false;
3526
3713
  } finally {
@@ -3559,6 +3746,10 @@ var client = new MediagraphClient({
3559
3746
  });
3560
3747
  var toolContext = { client };
3561
3748
  var resourceContext = { client };
3749
+ function getOrganizationSlug() {
3750
+ const stored = tokenStore.load();
3751
+ return stored?.organizationSlug;
3752
+ }
3562
3753
  var server = new Server(
3563
3754
  {
3564
3755
  name: "mediagraph-mcp",
@@ -3580,33 +3771,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3580
3771
  const { name, arguments: args } = request.params;
3581
3772
  let token = await getAccessToken();
3582
3773
  if (!token) {
3583
- const authSuccess = await runAutoAuth();
3584
- if (!authSuccess) {
3585
- return {
3586
- content: [
3587
- {
3588
- type: "text",
3589
- text: "Failed to authenticate with Mediagraph. Please try again or check your browser for the authorization window."
3590
- }
3591
- ],
3592
- isError: true
3593
- };
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
+ }
3594
3785
  }
3595
- token = await getAccessToken();
3596
3786
  if (!token) {
3597
- return {
3598
- content: [
3599
- {
3600
- type: "text",
3601
- text: "Authentication completed but failed to retrieve access token. Please try again."
3602
- }
3603
- ],
3604
- isError: true
3605
- };
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
+ }
3606
3811
  }
3607
3812
  }
3608
3813
  console.error(`[MCP] Tool call: ${name}`);
3609
3814
  console.error(`[MCP] Arguments: ${JSON.stringify(args, null, 2)}`);
3815
+ toolContext.organizationSlug = getOrganizationSlug();
3610
3816
  const result = await handleTool(name, args || {}, toolContext);
3611
3817
  if (result.isError) {
3612
3818
  console.error(`[MCP] Tool error: ${result.content[0]?.text}`);
@@ -3633,6 +3839,9 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
3633
3839
  });
3634
3840
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3635
3841
  const { uri } = request.params;
3842
+ if (uri.startsWith("ui://mediagraph/")) {
3843
+ return handleAppResource(uri);
3844
+ }
3636
3845
  let token = await getAccessToken();
3637
3846
  if (!token) {
3638
3847
  const authSuccess = await runAutoAuth();
@@ -3665,6 +3874,57 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3665
3874
  contents: [content]
3666
3875
  };
3667
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
+ }
3668
3928
  async function runAuthorize() {
3669
3929
  if (!config.clientId) {
3670
3930
  console.error("Error: MEDIAGRAPH_CLIENT_ID environment variable is required");
@@ -3691,18 +3951,25 @@ async function runAuthorize() {
3691
3951
  console.log("Tokens received successfully.");
3692
3952
  currentTokens = tokens;
3693
3953
  const whoami = await client.whoami();
3954
+ console.log("Whoami response:", JSON.stringify(whoami, null, 2));
3955
+ const org = whoami.organization;
3694
3956
  const storedData = {
3695
3957
  tokens,
3696
- organizationId: whoami.organization.id,
3697
- organizationName: whoami.organization.name,
3698
- userId: whoami.user.id,
3699
- 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
3700
3963
  };
3701
3964
  tokenStore.save(storedData);
3702
3965
  console.log("");
3703
3966
  console.log("Successfully authorized!");
3704
- console.log(`Organization: ${whoami.organization.name}`);
3705
- 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
+ }
3706
3973
  console.log("");
3707
3974
  console.log("You can now use the Mediagraph MCP server.");
3708
3975
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediagraph/mcp",
3
- "version": "1.0.10",
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
  }