@ourroadmaps/mcp 0.9.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/index.js +429 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -475,7 +475,8 @@ class ApiClient {
475
475
  });
476
476
  if (!response.ok) {
477
477
  const error = await response.json().catch(() => ({ message: response.statusText }));
478
- throw new Error(`API Error (${response.status}): ${error.error?.message || error.message}`);
478
+ const errorMessage = error.error?.message || error.message || "Unknown error";
479
+ throw new Error(`API Error (${response.status}): ${errorMessage} - Path: ${path}`);
479
480
  }
480
481
  if (response.status === 204) {
481
482
  return;
@@ -569,9 +570,9 @@ class ApiClient {
569
570
  return response.data;
570
571
  }
571
572
  async linkFeatureToRoadmap(featureId, roadmapId) {
572
- await this.request(`/v1/features/${featureId}/roadmap-items`, {
573
+ await this.request(`/v1/features/${featureId}/roadmap-links`, {
573
574
  method: "POST",
574
- body: JSON.stringify({ roadmapItemId: roadmapId })
575
+ body: JSON.stringify({ roadmapId })
575
576
  });
576
577
  }
577
578
  async listIdeas() {
@@ -734,6 +735,91 @@ class ApiClient {
734
735
  });
735
736
  return response.data;
736
737
  }
738
+ async getWireframeUploadUrl(data) {
739
+ const response = await this.request("/v1/wireframes/upload-url", {
740
+ method: "POST",
741
+ body: JSON.stringify(data)
742
+ });
743
+ return response.data;
744
+ }
745
+ async createWireframe(roadmapId, data) {
746
+ const response = await this.request(`/v1/wireframes/roadmaps/${roadmapId}/wireframes`, {
747
+ method: "POST",
748
+ body: JSON.stringify(data)
749
+ });
750
+ return response.data;
751
+ }
752
+ async updateWireframe(id, data) {
753
+ const response = await this.request(`/v1/wireframes/${id}`, {
754
+ method: "PATCH",
755
+ body: JSON.stringify(data)
756
+ });
757
+ return response.data;
758
+ }
759
+ async updateWireframeOutcomes(id, outcomeIds) {
760
+ const response = await this.request(`/v1/wireframes/${id}/outcomes`, {
761
+ method: "POST",
762
+ body: JSON.stringify({ outcomeIds })
763
+ });
764
+ return response.data;
765
+ }
766
+ async deleteWireframe(id) {
767
+ await this.request(`/v1/wireframes/${id}`, {
768
+ method: "DELETE"
769
+ });
770
+ }
771
+ async getBrainstormUploadUrl(data) {
772
+ const response = await this.request("/v1/brainstorm/upload-url", {
773
+ method: "POST",
774
+ body: JSON.stringify(data)
775
+ });
776
+ return response.data;
777
+ }
778
+ async createBrainstormMedia(roadmapId, data) {
779
+ const response = await this.request(`/v1/brainstorm/roadmaps/${roadmapId}/media`, {
780
+ method: "POST",
781
+ body: JSON.stringify(data)
782
+ });
783
+ return response.data;
784
+ }
785
+ async updateBrainstormMedia(id, data) {
786
+ const response = await this.request(`/v1/brainstorm/media/${id}`, {
787
+ method: "PATCH",
788
+ body: JSON.stringify(data)
789
+ });
790
+ return response.data;
791
+ }
792
+ async deleteBrainstormMedia(id) {
793
+ await this.request(`/v1/brainstorm/media/${id}`, {
794
+ method: "DELETE"
795
+ });
796
+ }
797
+ async getProductDesignUploadUrl(productId, data) {
798
+ const response = await this.request(`/v1/products/${productId}/design-media/upload-url`, {
799
+ method: "POST",
800
+ body: JSON.stringify(data)
801
+ });
802
+ return response.data;
803
+ }
804
+ async createProductDesignMedia(productId, data) {
805
+ const response = await this.request(`/v1/products/${productId}/design-media`, {
806
+ method: "POST",
807
+ body: JSON.stringify(data)
808
+ });
809
+ return response.data;
810
+ }
811
+ async updateProductDesignMedia(productId, mediaId, data) {
812
+ const response = await this.request(`/v1/products/${productId}/design-media/${mediaId}`, {
813
+ method: "PATCH",
814
+ body: JSON.stringify(data)
815
+ });
816
+ return response.data;
817
+ }
818
+ async deleteProductDesignMedia(productId, mediaId) {
819
+ await this.request(`/v1/products/${productId}/design-media/${mediaId}`, {
820
+ method: "DELETE"
821
+ });
822
+ }
737
823
  async updateDesignDescription(roadmapId, description) {
738
824
  const response = await this.request(`/v1/designs/roadmaps/${roadmapId}/design`, {
739
825
  method: "PATCH",
@@ -772,6 +858,20 @@ function getApiClient() {
772
858
  }
773
859
 
774
860
  // src/tools/index.ts
861
+ function inferContentType(filename) {
862
+ const ext = filename.split(".").pop()?.toLowerCase();
863
+ const mapping = {
864
+ png: "image/png",
865
+ jpg: "image/jpeg",
866
+ jpeg: "image/jpeg",
867
+ gif: "image/gif",
868
+ webp: "image/webp",
869
+ mp4: "video/mp4",
870
+ webm: "video/webm",
871
+ mov: "video/quicktime"
872
+ };
873
+ return ext ? mapping[ext] ?? null : null;
874
+ }
775
875
  function registerAllTools(server) {
776
876
  registerSearchRoadmaps(server);
777
877
  registerGetRoadmap(server);
@@ -813,6 +913,15 @@ function registerAllTools(server) {
813
913
  registerGetHistorySummary(server);
814
914
  registerCreateStatusUpdate(server);
815
915
  registerSaveStories(server);
916
+ registerUploadWireframe(server);
917
+ registerUpdateWireframe(server);
918
+ registerDeleteWireframe(server);
919
+ registerUploadBrainstormMedia(server);
920
+ registerUpdateBrainstormMedia(server);
921
+ registerDeleteBrainstormMedia(server);
922
+ registerUploadProductDesignMedia(server);
923
+ registerUpdateProductDesignMedia(server);
924
+ registerDeleteProductDesignMedia(server);
816
925
  }
817
926
  function registerSearchRoadmaps(server) {
818
927
  server.registerTool("search_roadmaps", {
@@ -2259,6 +2368,323 @@ function registerSaveStories(server) {
2259
2368
  };
2260
2369
  });
2261
2370
  }
2371
+ function registerUploadWireframe(server) {
2372
+ server.registerTool("upload_wireframe", {
2373
+ description: "Upload an image to a roadmap item's wireframes. Returns a curl command to execute for the upload, then creates the wireframe record. After running the curl command, the wireframe will be visible in the web app.",
2374
+ inputSchema: {
2375
+ roadmapId: z10.string().describe("The UUID of the roadmap item"),
2376
+ filePath: z10.string().describe("Absolute path to the image file on disk"),
2377
+ description: z10.string().optional().describe("Optional description of the wireframe"),
2378
+ outcomeIds: z10.array(z10.string()).optional().describe("Optional array of PRD outcome IDs this wireframe addresses")
2379
+ }
2380
+ }, async ({
2381
+ roadmapId,
2382
+ filePath,
2383
+ description,
2384
+ outcomeIds
2385
+ }) => {
2386
+ const client2 = getApiClient();
2387
+ const filename = filePath.split("/").pop() || "image.png";
2388
+ const contentType = inferContentType(filename);
2389
+ if (!contentType || !contentType.startsWith("image/")) {
2390
+ return {
2391
+ content: [
2392
+ {
2393
+ type: "text",
2394
+ text: JSON.stringify({
2395
+ error: "Invalid file type. Wireframes only accept image files (png, jpeg, gif, webp)."
2396
+ })
2397
+ }
2398
+ ]
2399
+ };
2400
+ }
2401
+ const { uploadUrl, imageUrl } = await client2.getWireframeUploadUrl({
2402
+ filename,
2403
+ contentType
2404
+ });
2405
+ const wireframe = await client2.createWireframe(roadmapId, { imageUrl });
2406
+ if (description) {
2407
+ await client2.updateWireframe(wireframe.id, { description });
2408
+ }
2409
+ if (outcomeIds && outcomeIds.length > 0) {
2410
+ await client2.updateWireframeOutcomes(wireframe.id, outcomeIds);
2411
+ }
2412
+ const curlCommand = `curl -X PUT -H "Content-Type: ${contentType}" --data-binary @"${filePath}" "${uploadUrl}"`;
2413
+ return {
2414
+ content: [
2415
+ {
2416
+ type: "text",
2417
+ text: JSON.stringify({
2418
+ wireframeId: wireframe.id,
2419
+ imageUrl,
2420
+ curlCommand,
2421
+ instructions: "Execute the curlCommand to upload the file. The wireframe record has been created and will display the image once uploaded."
2422
+ }, null, 2)
2423
+ }
2424
+ ]
2425
+ };
2426
+ });
2427
+ }
2428
+ function registerUpdateWireframe(server) {
2429
+ server.registerTool("update_wireframe", {
2430
+ description: "Update a wireframe's description or outcome tags.",
2431
+ inputSchema: {
2432
+ id: z10.string().describe("The UUID of the wireframe"),
2433
+ description: z10.string().nullable().optional().describe("New description for the wireframe"),
2434
+ outcomeIds: z10.array(z10.string()).optional().describe("Array of PRD outcome IDs this wireframe addresses (replaces existing)")
2435
+ }
2436
+ }, async ({
2437
+ id,
2438
+ description,
2439
+ outcomeIds
2440
+ }) => {
2441
+ const client2 = getApiClient();
2442
+ let wireframe;
2443
+ if (description !== undefined) {
2444
+ wireframe = await client2.updateWireframe(id, { description });
2445
+ } else {
2446
+ wireframe = await client2.updateWireframe(id, {});
2447
+ }
2448
+ if (outcomeIds !== undefined) {
2449
+ wireframe = await client2.updateWireframeOutcomes(id, outcomeIds);
2450
+ }
2451
+ return {
2452
+ content: [
2453
+ {
2454
+ type: "text",
2455
+ text: JSON.stringify({
2456
+ id: wireframe.id,
2457
+ imageUrl: wireframe.imageUrl,
2458
+ description: wireframe.description,
2459
+ outcomes: wireframe.outcomes.map((o) => o.prdOutcomeId)
2460
+ }, null, 2)
2461
+ }
2462
+ ]
2463
+ };
2464
+ });
2465
+ }
2466
+ function registerDeleteWireframe(server) {
2467
+ server.registerTool("delete_wireframe", {
2468
+ description: "Delete a wireframe (soft delete).",
2469
+ inputSchema: {
2470
+ id: z10.string().describe("The UUID of the wireframe to delete")
2471
+ }
2472
+ }, async ({ id }) => {
2473
+ const client2 = getApiClient();
2474
+ await client2.deleteWireframe(id);
2475
+ return {
2476
+ content: [
2477
+ {
2478
+ type: "text",
2479
+ text: JSON.stringify({ success: true, deleted: id })
2480
+ }
2481
+ ]
2482
+ };
2483
+ });
2484
+ }
2485
+ function registerUploadBrainstormMedia(server) {
2486
+ server.registerTool("upload_brainstorm_media", {
2487
+ description: "Upload an image or video to a roadmap item's brainstorming section. Returns a curl command to execute for the upload.",
2488
+ inputSchema: {
2489
+ roadmapId: z10.string().describe("The UUID of the roadmap item"),
2490
+ filePath: z10.string().describe("Absolute path to the image or video file on disk"),
2491
+ description: z10.string().optional().describe("Optional description of the media")
2492
+ }
2493
+ }, async ({
2494
+ roadmapId,
2495
+ filePath,
2496
+ description
2497
+ }) => {
2498
+ const client2 = getApiClient();
2499
+ const filename = filePath.split("/").pop() || "file.bin";
2500
+ const contentType = inferContentType(filename);
2501
+ if (!contentType) {
2502
+ return {
2503
+ content: [
2504
+ {
2505
+ type: "text",
2506
+ text: JSON.stringify({
2507
+ error: "Invalid file type. Brainstorm media accepts images (png, jpeg, gif, webp) and videos (mp4, webm, mov)."
2508
+ })
2509
+ }
2510
+ ]
2511
+ };
2512
+ }
2513
+ const mediaType = contentType.startsWith("image/") ? "image" : "video";
2514
+ const { uploadUrl, mediaUrl } = await client2.getBrainstormUploadUrl({
2515
+ filename,
2516
+ contentType
2517
+ });
2518
+ let media = await client2.createBrainstormMedia(roadmapId, { mediaUrl, mediaType });
2519
+ if (description) {
2520
+ media = await client2.updateBrainstormMedia(media.id, { description });
2521
+ }
2522
+ const curlCommand = `curl -X PUT -H "Content-Type: ${contentType}" --data-binary @"${filePath}" "${uploadUrl}"`;
2523
+ return {
2524
+ content: [
2525
+ {
2526
+ type: "text",
2527
+ text: JSON.stringify({
2528
+ mediaId: media.id,
2529
+ mediaUrl,
2530
+ mediaType,
2531
+ curlCommand,
2532
+ instructions: "Execute the curlCommand to upload the file. The media record has been created and will display once uploaded."
2533
+ }, null, 2)
2534
+ }
2535
+ ]
2536
+ };
2537
+ });
2538
+ }
2539
+ function registerUpdateBrainstormMedia(server) {
2540
+ server.registerTool("update_brainstorm_media", {
2541
+ description: "Update a brainstorm media item's description.",
2542
+ inputSchema: {
2543
+ id: z10.string().describe("The UUID of the brainstorm media"),
2544
+ description: z10.string().nullable().describe("New description for the media")
2545
+ }
2546
+ }, async ({ id, description }) => {
2547
+ const client2 = getApiClient();
2548
+ const media = await client2.updateBrainstormMedia(id, { description });
2549
+ return {
2550
+ content: [
2551
+ {
2552
+ type: "text",
2553
+ text: JSON.stringify({
2554
+ id: media.id,
2555
+ mediaUrl: media.mediaUrl,
2556
+ mediaType: media.mediaType,
2557
+ description: media.description
2558
+ }, null, 2)
2559
+ }
2560
+ ]
2561
+ };
2562
+ });
2563
+ }
2564
+ function registerDeleteBrainstormMedia(server) {
2565
+ server.registerTool("delete_brainstorm_media", {
2566
+ description: "Delete a brainstorm media item (soft delete).",
2567
+ inputSchema: {
2568
+ id: z10.string().describe("The UUID of the brainstorm media to delete")
2569
+ }
2570
+ }, async ({ id }) => {
2571
+ const client2 = getApiClient();
2572
+ await client2.deleteBrainstormMedia(id);
2573
+ return {
2574
+ content: [
2575
+ {
2576
+ type: "text",
2577
+ text: JSON.stringify({ success: true, deleted: id })
2578
+ }
2579
+ ]
2580
+ };
2581
+ });
2582
+ }
2583
+ function registerUploadProductDesignMedia(server) {
2584
+ server.registerTool("upload_product_design_media", {
2585
+ description: "Upload an image or video to a product's design media section. Returns a curl command to execute for the upload.",
2586
+ inputSchema: {
2587
+ productId: z10.string().describe("The UUID of the product"),
2588
+ filePath: z10.string().describe("Absolute path to the image or video file on disk"),
2589
+ description: z10.string().optional().describe("Optional description of the media")
2590
+ }
2591
+ }, async ({
2592
+ productId,
2593
+ filePath,
2594
+ description
2595
+ }) => {
2596
+ const client2 = getApiClient();
2597
+ const filename = filePath.split("/").pop() || "file.bin";
2598
+ const contentType = inferContentType(filename);
2599
+ if (!contentType) {
2600
+ return {
2601
+ content: [
2602
+ {
2603
+ type: "text",
2604
+ text: JSON.stringify({
2605
+ error: "Invalid file type. Design media accepts images (png, jpeg, gif, webp) and videos (mp4, webm, mov)."
2606
+ })
2607
+ }
2608
+ ]
2609
+ };
2610
+ }
2611
+ const mediaType = contentType.startsWith("image/") ? "image" : "video";
2612
+ const { uploadUrl, mediaUrl } = await client2.getProductDesignUploadUrl(productId, {
2613
+ filename,
2614
+ contentType
2615
+ });
2616
+ let media = await client2.createProductDesignMedia(productId, { mediaUrl, mediaType });
2617
+ if (description) {
2618
+ media = await client2.updateProductDesignMedia(productId, media.id, { description });
2619
+ }
2620
+ const curlCommand = `curl -X PUT -H "Content-Type: ${contentType}" --data-binary @"${filePath}" "${uploadUrl}"`;
2621
+ return {
2622
+ content: [
2623
+ {
2624
+ type: "text",
2625
+ text: JSON.stringify({
2626
+ mediaId: media.id,
2627
+ productId,
2628
+ mediaUrl,
2629
+ mediaType,
2630
+ curlCommand,
2631
+ instructions: "Execute the curlCommand to upload the file. The media record has been created and will display once uploaded."
2632
+ }, null, 2)
2633
+ }
2634
+ ]
2635
+ };
2636
+ });
2637
+ }
2638
+ function registerUpdateProductDesignMedia(server) {
2639
+ server.registerTool("update_product_design_media", {
2640
+ description: "Update a product design media item's description.",
2641
+ inputSchema: {
2642
+ productId: z10.string().describe("The UUID of the product"),
2643
+ mediaId: z10.string().describe("The UUID of the design media"),
2644
+ description: z10.string().nullable().describe("New description for the media")
2645
+ }
2646
+ }, async ({
2647
+ productId,
2648
+ mediaId,
2649
+ description
2650
+ }) => {
2651
+ const client2 = getApiClient();
2652
+ const media = await client2.updateProductDesignMedia(productId, mediaId, { description });
2653
+ return {
2654
+ content: [
2655
+ {
2656
+ type: "text",
2657
+ text: JSON.stringify({
2658
+ id: media.id,
2659
+ mediaUrl: media.mediaUrl,
2660
+ mediaType: media.mediaType,
2661
+ description: media.description
2662
+ }, null, 2)
2663
+ }
2664
+ ]
2665
+ };
2666
+ });
2667
+ }
2668
+ function registerDeleteProductDesignMedia(server) {
2669
+ server.registerTool("delete_product_design_media", {
2670
+ description: "Delete a product design media item (soft delete).",
2671
+ inputSchema: {
2672
+ productId: z10.string().describe("The UUID of the product"),
2673
+ mediaId: z10.string().describe("The UUID of the design media to delete")
2674
+ }
2675
+ }, async ({ productId, mediaId }) => {
2676
+ const client2 = getApiClient();
2677
+ await client2.deleteProductDesignMedia(productId, mediaId);
2678
+ return {
2679
+ content: [
2680
+ {
2681
+ type: "text",
2682
+ text: JSON.stringify({ success: true, deleted: mediaId })
2683
+ }
2684
+ ]
2685
+ };
2686
+ });
2687
+ }
2262
2688
 
2263
2689
  // src/index.ts
2264
2690
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ourroadmaps/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "MCP server for OurRoadmaps - manage roadmaps, features, and ideas from Claude Code",
5
5
  "type": "module",
6
6
  "bin": {