@projectsolo/solo-mission-mcp 0.22.1 → 0.23.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/README.md CHANGED
@@ -160,7 +160,7 @@ Free off-chain missions need no wallet. To fund or refund a paid mission, point
160
160
 
161
161
  ## Tools Reference
162
162
 
163
- 34 tools in total.
163
+ 33 tools in total.
164
164
 
165
165
  ### Agent (1 tool)
166
166
 
@@ -219,14 +219,15 @@ Free off-chain missions need no wallet. To fund or refund a paid mission, point
219
219
 
220
220
  **Limitation:** The buffers are in-memory only. If the session ends (client closes, process crashes), unread buffered messages are lost and watches stop. To recover, call `get_messages` with a manual `since` timestamp and re-watch.
221
221
 
222
- ### Media tracks (4 tools, `media_review` missions)
222
+ ### Media tracks (3 tools, `media_review` missions)
223
223
 
224
224
  | Tool | Description |
225
225
  |---|---|
226
226
  | `add_mission_track` | Upload an audio, image or video track, via `file_path` (use this for audio/video) or `file_base64` |
227
227
  | `list_mission_tracks` | List a mission's tracks with `vote_counts` and `total_listen_seconds` |
228
228
  | `get_track_ratings` | Per-participant star ratings and comments for one track |
229
- | `delete_mission_track` | Delete a track that has no ratings yet |
229
+
230
+ Tracks can't be deleted or replaced. To change the media, cancel the mission and create a new one: `cancel_mission` for an off-chain or unfunded (`pending_funding`) mission, or `refund_solana_mission` with `action: 'cancel'` for a funded one before its `qualify_deadline`. If an upload fails partway, retry `add_mission_track`: an unconfirmed track is ignored (not listed, not counted toward the 20-track cap or completion).
230
231
 
231
232
  ### Solana (4 tools)
232
233
 
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- apiDelete,
4
3
  apiGet,
5
4
  apiPatch,
6
5
  apiPost,
@@ -789,7 +788,7 @@ import { readFile } from "fs/promises";
789
788
  var trackTools = [
790
789
  {
791
790
  name: "add_mission_track",
792
- description: "Upload a media item (audio, image, or video) to a media_review mission. Provide the file EITHER via file_path (the MCP server reads it from local disk and uploads it directly \u2014 use this for audio/video, since inlining a multi-MB file as base64 in the tool call can exceed the calling agent's own tool-call or context limits, causing a silent client-side failure before any request reaches the API) OR inline as file_base64 (fine for small images). Exactly one of the two is required. For on-chain missions, call this BEFORE fund_solana_mission \u2014 uploads are blocked once the mission is active. For off-chain missions, call while the mission is active and before any participant is hired. The item becomes visible to hired participants once confirmed.",
791
+ description: "Upload a media item (audio, image, or video) to a media_review mission. Provide the file EITHER via file_path (the MCP server reads it from local disk and uploads it directly \u2014 use this for audio/video, since inlining a multi-MB file as base64 in the tool call can exceed the calling agent's own tool-call or context limits, causing a silent client-side failure before any request reaches the API) OR inline as file_base64 (fine for small images). Exactly one of the two is required. For on-chain missions, call this BEFORE fund_solana_mission \u2014 uploads are blocked once the mission is active. For off-chain missions, call while the mission is active and before any participant is hired. The item becomes visible to hired participants once confirmed. Tracks cannot be deleted or replaced, so check the file before uploading. To change the media, cancel the mission and create a new one: for an off-chain mission or an unfunded Solana mission (pending_funding), use cancel_mission; for a funded Solana mission, use refund_solana_mission with action 'cancel' before its qualify_deadline. If the upload fails partway, it is safe to call add_mission_track again: a track whose upload was never confirmed is ignored (not shown, not counted toward the 20-track cap, not required for completion).",
793
792
  inputSchema: {
794
793
  type: "object",
795
794
  properties: {
@@ -836,18 +835,6 @@ var trackTools = [
836
835
  },
837
836
  required: ["mission_id", "track_id"]
838
837
  }
839
- },
840
- {
841
- name: "delete_mission_track",
842
- description: "Delete a track from a mission. Only allowed if the track has received no ratings yet.",
843
- inputSchema: {
844
- type: "object",
845
- properties: {
846
- mission_id: { type: "string", description: "ID of the mission" },
847
- track_id: { type: "string", description: "ID of the track to delete" }
848
- },
849
- required: ["mission_id", "track_id"]
850
- }
851
838
  }
852
839
  ];
853
840
  async function handleTrackTool(name, args) {
@@ -881,9 +868,9 @@ async function handleTrackTool(name, args) {
881
868
  });
882
869
  if (!uploadRes.ok) {
883
870
  const errText = await uploadRes.text().catch(() => "");
884
- await apiDelete(`/agent/missions/${mission_id}/tracks/${urlRes.track_id}`).catch(() => {
885
- });
886
- throw new Error(`Media upload failed: ${uploadRes.status} ${errText}`);
871
+ throw new Error(
872
+ `Media upload failed: ${uploadRes.status} ${errText}. The unconfirmed track is ignored by the platform (not listed, not counted toward the 20-track cap), so it is safe to retry add_mission_track.`
873
+ );
887
874
  }
888
875
  const confirmed = await apiPost(
889
876
  `/agent/missions/${mission_id}/tracks/${urlRes.track_id}/confirm`,
@@ -899,10 +886,6 @@ async function handleTrackTool(name, args) {
899
886
  const { mission_id, track_id } = args;
900
887
  return apiGet(`/agent/missions/${mission_id}/tracks/${track_id}/ratings`);
901
888
  }
902
- case "delete_mission_track": {
903
- const { mission_id, track_id } = args;
904
- return apiDelete(`/agent/missions/${mission_id}/tracks/${track_id}`);
905
- }
906
889
  default:
907
890
  throw new Error(`Unknown track tool: ${name}`);
908
891
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectsolo/solo-mission-mcp",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "MCP server for SoloMission Platform — lets AI agents create missions, browse humans, and chat.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@ vi.mock('../api/client.js', () => ({
22
22
  apiDelete: (...args: unknown[]) => apiDelete(...args),
23
23
  }));
24
24
 
25
- import { handleTrackTool } from './tracks.js';
25
+ import { handleTrackTool, trackTools } from './tracks.js';
26
26
 
27
27
  describe('add_mission_track', () => {
28
28
  let tmpDir: string;
@@ -131,9 +131,8 @@ describe('add_mission_track', () => {
131
131
  expect(fetchMock).not.toHaveBeenCalled();
132
132
  });
133
133
 
134
- it('deletes the pending track doc when the signed-URL PUT fails', async () => {
134
+ it('does not try to delete the pending track when the signed-URL PUT fails, and says a retry is safe', async () => {
135
135
  mockHappyPathApi();
136
- apiDelete.mockResolvedValue(undefined);
137
136
  fetchMock.mockResolvedValue({ ok: false, status: 500, text: async () => 'boom' });
138
137
  const filePath = join(tmpDir, 'song.mp3');
139
138
  writeFileSync(filePath, 'bytes');
@@ -145,7 +144,19 @@ describe('add_mission_track', () => {
145
144
  content_type: 'audio/mpeg',
146
145
  file_path: filePath,
147
146
  }),
148
- ).rejects.toThrow(/Media upload failed/);
149
- expect(apiDelete).toHaveBeenCalledWith('/agent/missions/m1/tracks/trk_1');
147
+ ).rejects.toThrow(/Media upload failed: 500 boom.*safe to retry add_mission_track/);
148
+ // The backend's DELETE track route is gone; an unconfirmed track is ignored server-side.
149
+ expect(apiDelete).not.toHaveBeenCalled();
150
+ // No confirm call for a failed upload.
151
+ expect(apiPost).toHaveBeenCalledTimes(1);
152
+ });
153
+ });
154
+
155
+ describe('track tool set', () => {
156
+ it('no longer exposes delete_mission_track', async () => {
157
+ expect(trackTools.map((t) => t.name)).not.toContain('delete_mission_track');
158
+ await expect(handleTrackTool('delete_mission_track', { mission_id: 'm1', track_id: 't1' })).rejects.toThrow(
159
+ /Unknown track tool/,
160
+ );
150
161
  });
151
162
  });
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
3
- import { apiGet, apiPost, apiDelete } from '../api/client.js';
3
+ import { apiGet, apiPost } from '../api/client.js';
4
4
 
5
5
  export const trackTools: Tool[] = [
6
6
  {
@@ -13,7 +13,13 @@ export const trackTools: Tool[] = [
13
13
  'small images). Exactly one of the two is required. ' +
14
14
  'For on-chain missions, call this BEFORE fund_solana_mission — uploads are blocked once the mission is active. ' +
15
15
  'For off-chain missions, call while the mission is active and before any participant is hired. ' +
16
- 'The item becomes visible to hired participants once confirmed.',
16
+ 'The item becomes visible to hired participants once confirmed. ' +
17
+ 'Tracks cannot be deleted or replaced, so check the file before uploading. To change the media, cancel the ' +
18
+ 'mission and create a new one: for an off-chain mission or an unfunded Solana mission (pending_funding), use ' +
19
+ "cancel_mission; for a funded Solana mission, use refund_solana_mission with action 'cancel' before its " +
20
+ 'qualify_deadline. ' +
21
+ 'If the upload fails partway, it is safe to call add_mission_track again: a track whose upload was never ' +
22
+ 'confirmed is ignored (not shown, not counted toward the 20-track cap, not required for completion).',
17
23
  inputSchema: {
18
24
  type: 'object',
19
25
  properties: {
@@ -78,18 +84,6 @@ export const trackTools: Tool[] = [
78
84
  required: ['mission_id', 'track_id'],
79
85
  },
80
86
  },
81
- {
82
- name: 'delete_mission_track',
83
- description: 'Delete a track from a mission. Only allowed if the track has received no ratings yet.',
84
- inputSchema: {
85
- type: 'object',
86
- properties: {
87
- mission_id: { type: 'string', description: 'ID of the mission' },
88
- track_id: { type: 'string', description: 'ID of the track to delete' },
89
- },
90
- required: ['mission_id', 'track_id'],
91
- },
92
- },
93
87
  ];
94
88
 
95
89
  export async function handleTrackTool(name: string, args: Record<string, any>): Promise<unknown> {
@@ -125,8 +119,9 @@ export async function handleTrackTool(name: string, args: Record<string, any>):
125
119
  );
126
120
 
127
121
  // Step 2: Upload binary to the signed URL.
128
- // On failure, delete the pending track doc so it doesn't consume the 20-track
129
- // slot or appear as a ghost in list results.
122
+ // A failed upload leaves the track doc pending (unconfirmed). The backend ignores
123
+ // pending tracks: they are not listed, not counted toward the 20-track cap and not
124
+ // required for completion. Tracks cannot be deleted, so there is nothing to clean up.
130
125
  const uploadRes = await fetch(urlRes.upload_url, {
131
126
  method: 'PUT',
132
127
  headers: { 'Content-Type': content_type },
@@ -134,9 +129,11 @@ export async function handleTrackTool(name: string, args: Record<string, any>):
134
129
  });
135
130
  if (!uploadRes.ok) {
136
131
  const errText = await uploadRes.text().catch(() => '');
137
- // Best-effort cleanup — ignore errors so the original failure is surfaced
138
- await apiDelete(`/agent/missions/${mission_id}/tracks/${urlRes.track_id}`).catch(() => {});
139
- throw new Error(`Media upload failed: ${uploadRes.status} ${errText}`);
132
+ throw new Error(
133
+ `Media upload failed: ${uploadRes.status} ${errText}. ` +
134
+ 'The unconfirmed track is ignored by the platform (not listed, not counted toward the 20-track cap), ' +
135
+ 'so it is safe to retry add_mission_track.',
136
+ );
140
137
  }
141
138
 
142
139
  // Step 3: Confirm upload and optionally set duration
@@ -158,11 +155,6 @@ export async function handleTrackTool(name: string, args: Record<string, any>):
158
155
  return apiGet(`/agent/missions/${mission_id}/tracks/${track_id}/ratings`);
159
156
  }
160
157
 
161
- case 'delete_mission_track': {
162
- const { mission_id, track_id } = args;
163
- return apiDelete(`/agent/missions/${mission_id}/tracks/${track_id}`);
164
- }
165
-
166
158
  default:
167
159
  throw new Error(`Unknown track tool: ${name}`);
168
160
  }