MCPcopy Create free account
hub / github.com/adcontextprotocol/adcp / proposeContentForUser

Function proposeContentForUser

server/src/routes/content.ts:365–646  ·  view source on GitHub ↗
(
  user: ContentUser,
  request: ProposeContentRequest
)

Source from the content-addressed store, hash-verified

363 * Use this from internal services like Addie to bypass HTTP authentication.
364 */
365export async function proposeContentForUser(
366 user: ContentUser,
367 request: ProposeContentRequest
368): Promise<ProposeContentResult> {
369 const {
370 title,
371 subtitle,
372 content,
373 content_type = 'article',
374 external_url,
375 external_site_name,
376 excerpt,
377 category,
378 tags = [],
379 author_title: requestAuthorTitle,
380 featured_image_url,
381 content_origin = 'member',
382 collection,
383 authors,
384 status: requestedStatus,
385 } = request;
386
387 // Per-user rate check — bounds every entry path to proposeContentForUser,
388 // including Addie's MCP tool handler that bypasses HTTP middleware.
389 const rate = checkProposeRateLimit(user.id);
390 if (!rate.ok) {
391 const retrySeconds = Math.max(1, Math.ceil(rate.retryAfterMs / 1000));
392 logger.warn({ userId: user.id, retrySeconds }, 'proposeContentForUser rate-limited');
393 return {
394 success: false,
395 error: `Submission rate limit exceeded (${PROPOSE_MAX_PER_WINDOW} per ${PROPOSE_WINDOW_MS / 60000} minutes). Try again in ${retrySeconds} seconds.`,
396 };
397 }
398
399 // Validate required fields
400 if (!title) {
401 return { success: false, error: 'title is required' };
402 }
403
404 // Field length validation — mirror DB column limits so callers get a
405 // friendly 400 instead of the Postgres "value too long" → HTTP 500
406 // path we were hitting (see #2734). The DB schema is authoritative;
407 // these constants just surface the limit before we hit the insert.
408 if (title.length > 500) {
409 return { success: false, error: `title is too long (max 500 characters; got ${title.length})` };
410 }
411 if (subtitle && subtitle.length > 1000) {
412 return { success: false, error: `subtitle is too long (max 1000 characters; got ${subtitle.length})` };
413 }
414 if (requestAuthorTitle && requestAuthorTitle.length > 255) {
415 return { success: false, error: `author_title is too long (max 255 characters; got ${requestAuthorTitle.length})` };
416 }
417 if (external_site_name && external_site_name.length > 255) {
418 return { success: false, error: `external_site_name is too long (max 255 characters; got ${external_site_name.length})` };
419 }
420
421 // Support both old format (collection.type + committee_slug) and new format (just committee_slug)
422 const committeeSlug = collection?.committee_slug || collection?.slug;

Callers 5

publishAsPerspectiveFunction · 0.85
createContentRouterFunction · 0.85
createMemberToolHandlersFunction · 0.85

Calls 14

awardPointsMethod · 0.95
checkAndAwardBadgesMethod · 0.95
checkProposeRateLimitFunction · 0.85
getPoolFunction · 0.85
isCommitteeLeadFunction · 0.85
isWebUserAAOAdminFunction · 0.85
getUserInfoFunction · 0.85
computeJourneyStageFunction · 0.85
notifyPendingReviewFunction · 0.85
notifyPublishedPostFunction · 0.85
warnMethod · 0.80

Tested by

no test coverage detected