Files
garfieldheron 0b9d863b38 docs(01-linkedin-video-upload): create phase plan
Phase 01: LinkedIn video upload support
- 2 plans created (01-01, 01-02)
- 5 tasks total: client upload flow, createVideoPost, MCP tool definition, REST route, e2e verify checkpoint
- Ready for execution
2026-05-11 12:14:31 -04:00

178 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 01-linkedin-video-upload
plan: 02
type: execute
depends_on: ["01-01"]
files_modified:
- src/tools.ts
- src/index.ts
---
<objective>
Wire createVideoPost() into the MCP tool layer and REST API.
Purpose: The upload client from Plan 01 is only callable internally. This plan exposes
it as a named MCP tool (`linkedin_upload_video`) and a REST endpoint
(`POST /api/linkedin/video`) so Claude and external callers can use it.
Output: A fully registered MCP tool + REST route that accepts video_url, text, visibility,
and account, and returns { success, post_id, url } — matching the pattern of
linkedin_create_post.
</objective>
<execution_context>
~/.claude/get-shit-done/workflows/execute-plan.md
./summary.md
~/.claude/get-shit-done/references/checkpoints.md
</execution_context>
<context>
@src/tools.ts
@src/index.ts
@src/clients/linkedin.ts
**Established patterns to follow exactly:**
tools.ts tool definition (copy linkedin_create_post structure, lines ~256-274):
{ name, description, inputSchema: { type: 'object', properties: {...}, required: [...] } }
tools.ts import (line 7):
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, ... } from './clients/linkedin.js';
→ add createVideoPost to this import
tools.ts handler (lines ~778-787, inside handleToolCall switch):
case 'linkedin_upload_video':
result = await createLinkedInVideoPost({ ... }, customer);
break;
index.ts REST route (lines ~747-756, follow app.post('/api/linkedin/post') pattern):
app.post('/api/linkedin/video', requireAuth, async (req, res) => { ... })
→ calls handleToolCall('linkedin_upload_video', { video_url, text, visibility, account })
**Tool input schema fields:**
video_url: { type: 'string', description: 'Publicly accessible URL of the video file to upload' } — required
text: { type: 'string', description: 'Post caption / commentary text' } — required
visibility: { type: 'string', enum: ['PUBLIC','CONNECTIONS'], description: 'Post visibility. Default: PUBLIC' }
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' }
</context>
<tasks>
<task type="auto">
<name>Task 1: Register linkedin_upload_video MCP tool in tools.ts</name>
<files>src/tools.ts</files>
<action>
1. Add `createVideoPost as createLinkedInVideoPost` to the existing linkedin import on line 7.
Keep all other imports on that line unchanged.
2. In the `tools` array, insert a new tool definition immediately after the
`linkedin_create_post` entry (around line 274). Use this exact structure:
{
name: 'linkedin_upload_video',
description: 'Upload a video and create a LinkedIn post. Accepts a publicly accessible video URL, downloads it server-side, uploads it to LinkedIn via the Videos API, and publishes the post. Video must be publicly reachable (not localhost or private network). Large videos may take 3090 seconds.',
inputSchema: {
type: 'object',
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to upload' },
text: { type: 'string', description: 'Post caption / commentary text' },
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Post visibility. Default: PUBLIC' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
required: ['video_url', 'text'],
},
}
3. In handleToolCall switch, add the handler case immediately after 'linkedin_create_post':
case 'linkedin_upload_video':
result = await createLinkedInVideoPost(
{
video_url: args.video_url as string,
text: args.text as string,
visibility: (args.visibility as 'PUBLIC' | 'CONNECTIONS') ?? 'PUBLIC',
account: args.account as string | undefined,
},
customer
);
break;
</action>
<verify>npx tsc --noEmit passes; grep -n 'linkedin_upload_video' src/tools.ts shows both the definition and the case handler</verify>
<done>Tool appears in tools array and switch statement, tsc clean</done>
</task>
<task type="auto">
<name>Task 2: Add POST /api/linkedin/video REST route in index.ts</name>
<files>src/index.ts</files>
<action>
Insert immediately after the `app.post('/api/linkedin/post', ...)` block (around line 756):
app.post('/api/linkedin/video', requireAuth, async (req, res) => {
try {
const { video_url, text, visibility, account } = req.body;
const result = await handleToolCall('linkedin_upload_video', { video_url, text, visibility, account });
res.json(result);
} catch (err) {
res.status(500).json({ error: String(err) });
}
});
Do NOT add input validation beyond what handleToolCall already does — the tool's
required fields check ('video_url', 'text') already throws if missing.
</action>
<verify>npx tsc --noEmit passes; grep -n '/api/linkedin/video' src/index.ts shows the new route</verify>
<done>Route registered, tsc clean, follows exact pattern of adjacent routes</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Full linkedin_upload_video tool: client (uploadVideo + createVideoPost), MCP tool definition,
REST route POST /api/linkedin/video. The server fetches the video from video_url, uploads
it to LinkedIn in 4MB chunks, waits up to 90s for LinkedIn to process it, then publishes
the post and returns { success, post_id, url }.
</what-built>
<how-to-verify>
1. Build: npm run build — must complete without errors
2. Start server: npm start (or node dist/index.js) in a separate terminal
3. Test the MCP tool via Claude by saying:
"Upload the SquareMCP LinkedIn video to LinkedIn with text [your caption]"
and providing the video_url pointing to a publicly accessible MP4
(or use the rendered out/squaremcp-linkedin.mp4 hosted somewhere accessible)
4. Confirm the response contains { success: true, post_id: "urn:li:share:...", url: "https://www.linkedin.com/feed/update/..." }
5. Visit the returned URL in a browser and confirm the video post appears with correct caption
6. Check the audit log (if Redis is running) shows the linkedin:createVideoPost entry
</how-to-verify>
<resume-signal>Type "approved" if the post appeared on LinkedIn with video, or describe the error to fix</resume-signal>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npm run build` (full TypeScript build) succeeds with zero errors
- [ ] `grep -n 'linkedin_upload_video' src/tools.ts` returns two matches (definition + case)
- [ ] `grep -n '/api/linkedin/video' src/index.ts` returns one match
- [ ] `grep -n 'createLinkedInVideoPost' src/tools.ts` returns two matches (import + call)
</verification>
<success_criteria>
- linkedin_upload_video appears in MCP tool list (visible to Claude at http://localhost:3000/mcp)
- POST /api/linkedin/video route returns { success, post_id, url } on valid input
- Error from missing video_url or text propagates as 500 with { error: "..." }
- Build is clean — no TypeScript errors in any file
- Phase complete: LinkedIn video upload fully wired from URL → LinkedIn post
</success_criteria>
<output>
After completion, create `.planning/phases/01-linkedin-video-upload/01-02-SUMMARY.md`:
# Phase 01 Plan 02: Tool + Route Wiring Summary
**[one-liner of what shipped]**
## Accomplishments
## Files Created/Modified
## Decisions Made
## Issues Encountered
## Next Step
Phase complete — linkedin_upload_video tool is live.
</output>