Commit graph

15 commits

Author SHA1 Message Date
Paulo Castellano
4d8353d758
MCP: workspace settings, viewer read access, and token access (#241)
* Add workspace MCP settings and token access controls.

Ship MCP settings UI, OAuth revoke/list helpers, Passport deploy wiring,
and workspace.token:mcp gating so assistants can connect without pulling
in welcome/onboarding from the parent epic.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Type MCP client config shapes instead of string checks.

Encode http/config-root on each advanced client and tighten primary
client ids so snippet generation does not branch on magic strings.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish MCP settings follow-ups from review.

Translate Ukrainian MCP copy, deep-link ChatGPT into connector
creation, drop an unused asset and revoke arg, and assert PATs are
rejected on the MCP endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden MCP connected clients, revoke scope, and OAuth consent.

List recoverable sessions with live refresh tokens, revoke only PATs,
throttle registration alone, and block viewers from authorizing MCP.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify MCP OAuth route throttling to a single middleware group.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Allow workspace viewers read-only MCP access with web policy writes.

Mirror the web app: MCP connects on view + OAuth mcp:use, write tools
enforce createPost/update/delete/manageAccounts/manageTeam, and demotion
to Viewer keeps grants. Cover role denials, consent, and disconnect.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden MCP tool authz with shared workspace helpers.

Route ApiKey tools through AuthorizesMcpTool, fail closed on null user
or policy argument, and resolve the current workspace before mutating.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop redundant string casts on validated request data.

Enum::from and validated() fields are already strings, so the casts
add noise without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Show only the current user's MCP connections in settings.

Match API keys privacy: list and disconnect your own OAuth clients,
not teammates' across the account.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Cover LoadWorkspaceFromToken gaps and harden AuthorizesMcpTool tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop redundant is_string guard before UpdatePostTool find.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor AppSidebar to always show MCP link and simplify route middleware definition in ai.php. The MCP link is now consistently displayed regardless of the current workspace state, and the route middleware syntax has been streamlined.

* Refresh MCP connected clients with Inertia usePoll.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bump laravel/mcp to 0.9.1 and add the TryPost server icon.

Requires laravel/boost 2.5 for the Icon attribute; expose images/trypost/icon.png on TryPostServer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop no-op ReflectionClass import in TryPostServerTest.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 09:54:51 -03:00
Paulo Castellano
2248d01edc
Add optional Pinterest pin title and destination link (#232)
* Add optional Pinterest pin title, description, and link.

Expose title/description/link across web, API, and MCP; seed description from caption into meta on save, and publish description only from meta.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Expand Pinterest title/description/link test coverage.

Cover web draft persistence and validation bounds, API/MCP update merge and seed, and publisher payload fields on video and carousel pins.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Pinterest: description is post content again.

Keep optional title and link in meta/settings only. Remove the separate description textarea, seed logic, and meta.description path so Pinterest follows the shared caption pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor Pinterest meta handling and validation.

- Update CreatePost and UpdatePost actions to filter out null values from meta fields.
- Introduce a new method in PinterestPublisher to resolve board IDs, ensuring required fields are validated.
- Enhance PinterestSettings component to manage title and link inputs, including validation for HTTP URLs.
- Update PostPlatformMetaRules to enforce URL validation for Pinterest links.
- Add tests for clearing Pinterest title and link, and for rejecting invalid links during scheduling.

This refactor improves the handling of Pinterest metadata and enhances user experience by ensuring proper validation and error handling.

* Add validation messages and attributes for Pinterest meta fields

- Introduced custom validation messages and friendly attribute names for Pinterest link and title fields in PostPlatformMetaRules.
- Updated StorePostRequest, UpdatePostRequest, and related tools to utilize these new messages and attributes.
- Enhanced tests to assert correct error messages for invalid Pinterest links and title length constraints.

This update improves user feedback during post creation and editing, ensuring clarity in validation errors.

* Remove click.prevent directive from Pinterest link in PinterestPreview component.

This change simplifies the link behavior, allowing default click actions to occur, which may enhance user interaction with the Pinterest link.

* Update validation error messages for Pinterest meta fields in tests

- Refined the assertions in PostApiPlatformMetaTest to include localized validation messages for Pinterest title and link fields.
- Ensured that error messages reflect the updated validation rules, enhancing clarity for users during post creation and editing.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:15:50 -03:00
Matteo Martini
a33ff5d00f
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default

* Align schedule validation and keep drafts unscheduled.

Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.

* Polish schedule validation typing and tests.

Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.

* Centralize status helper in post update validation.

Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.

* Share scheduled_at update rules across web, API, and MCP.

Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.

* Cover the full unscheduled-draft checklist in Pest.

Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.

* Remove normalizeStatus helper.

Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop is_string status guards from schedule validation.

Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 17:39:18 -03:00
Paulo Castellano
b28b18ef72 Address PR review: stream MCP uploads and close listing gaps.
Stream signed uploads through addMediaFromPath, return per-type max_bytes, harden Pinterest/Discord listing errors and pagination, and keep frontend duration fallbacks when Inertia once-props have not synced.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 22:00:07 -03:00
Paulo Castellano
ff0bfa8a06 Expose Pinterest board IDs via MCP and REST API.
Agents need board_id to publish pins; list boards per connected account so create/update can set platforms[].meta.board_id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 21:47:51 -03:00
Paulo Castellano
06f83a6571 Track post creation origin via created_via.
Persist whether a post was created through web, MCP, API, or automation so we can attribute entry points without guessing from request context.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:19:01 -03:00
Paulo Castellano
ba20534a6d refactor(meta): own the platforms.*.meta parent rule in PostPlatformMetaRules
The shared rules() returned only meta sub-keys, so every caller still repeated
its own 'platforms.*.meta' => array parent (with slightly different sometimes/
nullable combos). Fold the parent into rules() with a single safe contract so
callers just spread one source for the whole meta block.
2026-06-16 17:50:40 -03:00
Paulo Castellano
bdea56d5e1 feat(api,mcp): full per-platform meta parity for posts
Centralize per-platform PostPlatform.meta validation in PostPlatformMetaRules
(shared by web, REST API and MCP). The API and MCP previously only accepted
aspect_ratio, silently stripping channel_id/board_id/privacy_level and the rest
via validated(), so Discord/Pinterest/TikTok couldn't be configured or published
through those entry points. Now all per-platform meta is accepted and persisted,
required-on-publish is enforced on API update (TikTok privacy, Pinterest board,
Discord channel), and the MCP publish tool guards a post's stored meta before
dispatching. Adds API + MCP tests and a PostPlatform discord() factory state.
2026-06-16 17:39:07 -03:00
Paulo Castellano
ce6ee04883 Bring post platform meta (aspect ratio) to parity across API and MCP
PR #82 made `meta.aspect_ratio` crop Facebook (and already Instagram) feed
images at publish time, but the API and MCP surfaces only half-supported it:
you couldn't set meta at creation, the value wasn't validated, and responses
never returned it. This closes those gaps.

- New `AspectRatio` enum is the single source of truth for the allowed ratios
  (1:1, 4:5, 16:9, original). App/API/MCP requests now validate via
  `Rule::enum(AspectRatio::class)` — an invalid ratio is rejected everywhere
  instead of silently center-cropping to square.
- API `StorePostRequest` and MCP `CreatePostTool` now accept `platforms.*.meta`;
  `CreatePost` persists it. MCP create documents `meta` in its schema.
- `Api\PostPlatformResource` now exposes `meta`, so API and MCP responses return
  the aspect_ratio (and other per-platform meta) a client set.
2026-06-10 15:36:47 -03:00
Paulo Castellano
b10f25f94d refactor(posts): tighten content-length caps and simplify contentOverflow
- Platform::contentOverflow returns int (0 = fits) via max(), drops the
  nullable-int + ternary pattern; HasSocialHttpClient and the validation
  rule updated to compare against 0 instead of null.
- Drop the hardcoded 63206-char limit to 10000 across all entry points
  (Platform enum, MCP CreatePostTool/UpdatePostTool, FacebookRules, all
  three FormRequests). Facebook's API accepts up to 63206 but nobody
  writes 63k-char posts and emoji-heavy content risks overflowing the
  TEXT column's 65535-byte ceiling.
- Compiled i18n JSON regenerated with the content_exceeds_platform key.
2026-05-11 20:06:44 -03:00
Paulo Castellano
c1418c9d21 fix: address PR review findings — publish, REST store, SSRF, race
Code-review surfaced two correctness bugs and a security gap that
needed to land before merging.

- UpdatePost::execute disabled every platform when called without
  a `platforms` key. PublishPostTool relied on that path, so every
  publish-via-MCP queued a job whose handler then found nothing
  enabled to publish to. Wrap the platform toggle in
  `Arr::has($data, 'platforms')` (matches the existing label_ids
  guard a few lines up). Add a regression assertion to
  `PostPublishToolTest::publish post immediate dispatches PublishPost
  job` that the previously-enabled platform stays enabled.

- StorePostRequest declared rules for only `platforms`,
  `scheduled_at`, and `status`. `validated()` then stripped
  `content`, `media`, and `label_ids`, so REST `POST /api/posts`
  silently created empty drafts. Added rules for content / media /
  label_ids (with workspace-scoped `Rule::exists` for labels) and
  dropped the unused `status` field — REST callers transition state
  via `PUT /posts/{id}`. Removed the dead `platforms.*.content`
  rule. Added a feature test that asserts content + media + labels
  roundtrip on create, plus a regression that an `is_active=false`
  social_account is rejected at validation.

- CreatePost::execute now syncs label_ids itself so REST and MCP
  share the behavior. Removed the duplicate sync from CreatePostTool.

- MCP UpdatePostTool didn't scope `platforms.*.id` to the post being
  updated, drifting from the REST UpdatePostRequest which adds
  `Rule::exists('post_platforms','id')->where('post_id', ...)`. Now
  it loads the post first (failing fast with `Post not found.` if
  the workspace check rejects), then uses the same Rule::exists.

- MediaAttacher fetched any URL the caller passed, including
  loopback / link-local / private targets — classic SSRF pivot.
  Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP
  ranges, and DNS hostnames whose A/AAAA records resolve into those
  ranges (covers DNS rebinding). Bypassed under
  `app()->runningUnitTests()` so `Http::fake()` keeps working.
  Streaming the response body lets us abort early once we exceed
  MAX_BYTES instead of buffering the full payload first; redirects
  are disabled so a 200→302 trick can't bypass the host check.

- The `media[]` JSON column had a lost-update race in
  `attachFromUrls`: read `$post->media`, mutate in PHP, write back.
  Two concurrent calls clobbered each other. Now wrapped in a
  transaction with `lockForUpdate()`.

- ESLint: `resources/js/actions/**` and `resources/js/routes/**`
  are auto-generated by Wayfinder on every build. Their import
  order matches PHP scan order, not alphabetical, so import/order
  fought eslint-fix forever. Added them to ignores.
2026-05-04 12:16:39 -03:00
Paulo Castellano
9a26e6d802 feat: complete create + publish post flow via MCP and REST API
Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of
a post — create with platform selection, attach media from URLs, schedule or
publish immediately, and fetch engagement metrics — without touching the web UI.

MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool,
ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now
accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains
status/search/limit filters.

REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics,
GET /api/posts/{post}/preview, GET /api/content-types.

Also fixes a silent CreatePost::execute bug — the action validated platforms[]
but ignored it, so REST callers never saw their selection persisted. Adds cross
validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform)
so a LinkedIn account can't be saddled with x_post, and rejects inactive social
accounts during validation instead of failing silently downstream.

Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both
MCP tools and REST controllers so behaviour stays aligned. New Resources
(PlatformContentTypesResource, PostMetricsResource, PostPreviewResource,
PostMediaAttachResource) keep controllers free of inline model mapping.

Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST
(PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and
the publish job (PublishToSocialPlatformTest).

Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 08:12:28 -03:00
Paulo Castellano
137cfb4f6e refactor: standardize MCP tool responses using API resources and enforce strict structural validation in feature tests 2026-05-03 21:34:20 -03:00
Paulo Castellano
c020419db1 feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 01:42:39 -03:00
Paulo Castellano
fabce14aad feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
  Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
  Label (List, Create, Update, Delete), Workspace (Get),
  ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 20:30:36 -03:00