@hasna/microservices 0.0.4 → 0.0.5

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 (38) hide show
  1. package/microservices/microservice-ads/src/cli/index.ts +198 -0
  2. package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
  3. package/microservices/microservice-ads/src/mcp/index.ts +160 -0
  4. package/microservices/microservice-contracts/src/cli/index.ts +410 -23
  5. package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
  6. package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
  7. package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
  8. package/microservices/microservice-domains/src/cli/index.ts +253 -0
  9. package/microservices/microservice-domains/src/db/domains.ts +613 -0
  10. package/microservices/microservice-domains/src/index.ts +21 -0
  11. package/microservices/microservice-domains/src/mcp/index.ts +168 -0
  12. package/microservices/microservice-hiring/src/cli/index.ts +318 -8
  13. package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
  14. package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
  15. package/microservices/microservice-hiring/src/index.ts +29 -0
  16. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  17. package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
  18. package/microservices/microservice-payments/src/cli/index.ts +255 -3
  19. package/microservices/microservice-payments/src/db/migrations.ts +18 -0
  20. package/microservices/microservice-payments/src/db/payments.ts +552 -0
  21. package/microservices/microservice-payments/src/mcp/index.ts +223 -0
  22. package/microservices/microservice-payroll/src/cli/index.ts +269 -0
  23. package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
  24. package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
  25. package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
  26. package/microservices/microservice-shipping/src/cli/index.ts +211 -3
  27. package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
  28. package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
  29. package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
  30. package/microservices/microservice-social/src/cli/index.ts +244 -2
  31. package/microservices/microservice-social/src/db/migrations.ts +33 -0
  32. package/microservices/microservice-social/src/db/social.ts +378 -4
  33. package/microservices/microservice-social/src/mcp/index.ts +221 -1
  34. package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
  35. package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
  36. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
  37. package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
  38. package/package.json +1 -1
@@ -7,7 +7,46 @@ import { getDatabase } from "./database.js";
7
7
  // ---- Types ----
8
8
 
9
9
  export type Platform = "x" | "linkedin" | "instagram" | "threads" | "bluesky";
10
- export type PostStatus = "draft" | "scheduled" | "published" | "failed";
10
+ export type PostStatus = "draft" | "scheduled" | "published" | "failed" | "pending_review";
11
+
12
+ export type Recurrence = "daily" | "weekly" | "biweekly" | "monthly";
13
+
14
+ /**
15
+ * Platform character limits for post content validation
16
+ */
17
+ export const PLATFORM_LIMITS: Record<Platform, number> = {
18
+ x: 280,
19
+ linkedin: 3000,
20
+ instagram: 2200,
21
+ threads: 500,
22
+ bluesky: 300,
23
+ };
24
+
25
+ export interface PlatformLimitWarning {
26
+ platform: Platform;
27
+ limit: number;
28
+ content_length: number;
29
+ over_by: number;
30
+ }
31
+
32
+ /**
33
+ * Check if content exceeds platform character limit for a given account
34
+ */
35
+ export function checkPlatformLimit(content: string, accountId: string): PlatformLimitWarning | null {
36
+ const account = getAccount(accountId);
37
+ if (!account) return null;
38
+
39
+ const limit = PLATFORM_LIMITS[account.platform];
40
+ if (content.length > limit) {
41
+ return {
42
+ platform: account.platform,
43
+ limit,
44
+ content_length: content.length,
45
+ over_by: content.length - limit,
46
+ };
47
+ }
48
+ return null;
49
+ }
11
50
 
12
51
  export interface Account {
13
52
  id: string;
@@ -59,6 +98,7 @@ export interface Post {
59
98
  platform_post_id: string | null;
60
99
  engagement: Engagement;
61
100
  tags: string[];
101
+ recurrence: Recurrence | null;
62
102
  created_at: string;
63
103
  updated_at: string;
64
104
  }
@@ -74,6 +114,7 @@ interface PostRow {
74
114
  platform_post_id: string | null;
75
115
  engagement: string;
76
116
  tags: string;
117
+ recurrence: string | null;
77
118
  created_at: string;
78
119
  updated_at: string;
79
120
  }
@@ -85,6 +126,7 @@ function rowToPost(row: PostRow): Post {
85
126
  media_urls: JSON.parse(row.media_urls || "[]"),
86
127
  engagement: JSON.parse(row.engagement || "{}"),
87
128
  tags: JSON.parse(row.tags || "[]"),
129
+ recurrence: (row.recurrence as Recurrence) || null,
88
130
  };
89
131
  }
90
132
 
@@ -256,6 +298,7 @@ export interface CreatePostInput {
256
298
  status?: PostStatus;
257
299
  scheduled_at?: string;
258
300
  tags?: string[];
301
+ recurrence?: Recurrence;
259
302
  }
260
303
 
261
304
  export function createPost(input: CreatePostInput): Post {
@@ -265,8 +308,8 @@ export function createPost(input: CreatePostInput): Post {
265
308
  const tags = JSON.stringify(input.tags || []);
266
309
 
267
310
  db.prepare(
268
- `INSERT INTO posts (id, account_id, content, media_urls, status, scheduled_at, tags)
269
- VALUES (?, ?, ?, ?, ?, ?, ?)`
311
+ `INSERT INTO posts (id, account_id, content, media_urls, status, scheduled_at, tags, recurrence)
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
270
313
  ).run(
271
314
  id,
272
315
  input.account_id,
@@ -274,7 +317,8 @@ export function createPost(input: CreatePostInput): Post {
274
317
  media_urls,
275
318
  input.status || "draft",
276
319
  input.scheduled_at || null,
277
- tags
320
+ tags,
321
+ input.recurrence || null
278
322
  );
279
323
 
280
324
  return getPost(id)!;
@@ -348,6 +392,7 @@ export interface UpdatePostInput {
348
392
  platform_post_id?: string;
349
393
  engagement?: Engagement;
350
394
  tags?: string[];
395
+ recurrence?: Recurrence | null;
351
396
  }
352
397
 
353
398
  export function updatePost(id: string, input: UpdatePostInput): Post | null {
@@ -390,6 +435,10 @@ export function updatePost(id: string, input: UpdatePostInput): Post | null {
390
435
  sets.push("tags = ?");
391
436
  params.push(JSON.stringify(input.tags));
392
437
  }
438
+ if (input.recurrence !== undefined) {
439
+ sets.push("recurrence = ?");
440
+ params.push(input.recurrence);
441
+ }
393
442
 
394
443
  if (sets.length === 0) return existing;
395
444
 
@@ -670,3 +719,328 @@ export function getOverallStats(): {
670
719
  engagement,
671
720
  };
672
721
  }
722
+
723
+ // ---- Bulk Schedule ----
724
+
725
+ export interface BatchScheduleInput {
726
+ account_id: string;
727
+ content: string;
728
+ scheduled_at: string;
729
+ media_urls?: string[];
730
+ tags?: string[];
731
+ recurrence?: Recurrence;
732
+ }
733
+
734
+ export interface BatchScheduleResult {
735
+ scheduled: Post[];
736
+ errors: { index: number; error: string }[];
737
+ warnings: PlatformLimitWarning[];
738
+ }
739
+
740
+ /**
741
+ * Schedule multiple posts at once from an array of post definitions
742
+ */
743
+ export function batchSchedule(posts: BatchScheduleInput[]): BatchScheduleResult {
744
+ const scheduled: Post[] = [];
745
+ const errors: { index: number; error: string }[] = [];
746
+ const warnings: PlatformLimitWarning[] = [];
747
+
748
+ for (let i = 0; i < posts.length; i++) {
749
+ const input = posts[i];
750
+ try {
751
+ // Validate account exists
752
+ const account = getAccount(input.account_id);
753
+ if (!account) {
754
+ errors.push({ index: i, error: `Account '${input.account_id}' not found` });
755
+ continue;
756
+ }
757
+
758
+ // Check platform limit
759
+ const warning = checkPlatformLimit(input.content, input.account_id);
760
+ if (warning) {
761
+ warnings.push(warning);
762
+ }
763
+
764
+ const post = createPost({
765
+ account_id: input.account_id,
766
+ content: input.content,
767
+ media_urls: input.media_urls,
768
+ status: "scheduled",
769
+ scheduled_at: input.scheduled_at,
770
+ tags: input.tags,
771
+ recurrence: input.recurrence,
772
+ });
773
+
774
+ scheduled.push(post);
775
+ } catch (err) {
776
+ errors.push({ index: i, error: err instanceof Error ? err.message : String(err) });
777
+ }
778
+ }
779
+
780
+ return { scheduled, errors, warnings };
781
+ }
782
+
783
+ // ---- Cross-Post ----
784
+
785
+ export interface CrossPostResult {
786
+ posts: Post[];
787
+ warnings: PlatformLimitWarning[];
788
+ }
789
+
790
+ /**
791
+ * Create identical post on multiple platform accounts
792
+ */
793
+ export function crossPost(
794
+ content: string,
795
+ platforms: Platform[],
796
+ options?: { media_urls?: string[]; tags?: string[]; scheduled_at?: string }
797
+ ): CrossPostResult {
798
+ const posts: Post[] = [];
799
+ const warnings: PlatformLimitWarning[] = [];
800
+
801
+ for (const platform of platforms) {
802
+ // Find an account for this platform
803
+ const accounts = listAccounts({ platform });
804
+ if (accounts.length === 0) {
805
+ throw new Error(`No account found for platform '${platform}'`);
806
+ }
807
+
808
+ const account = accounts[0]; // Use first matching account
809
+
810
+ // Check platform limit
811
+ const limit = PLATFORM_LIMITS[platform];
812
+ if (content.length > limit) {
813
+ warnings.push({
814
+ platform,
815
+ limit,
816
+ content_length: content.length,
817
+ over_by: content.length - limit,
818
+ });
819
+ }
820
+
821
+ const post = createPost({
822
+ account_id: account.id,
823
+ content,
824
+ media_urls: options?.media_urls,
825
+ status: options?.scheduled_at ? "scheduled" : "draft",
826
+ scheduled_at: options?.scheduled_at,
827
+ tags: options?.tags,
828
+ });
829
+
830
+ posts.push(post);
831
+ }
832
+
833
+ return { posts, warnings };
834
+ }
835
+
836
+ // ---- Best Time to Post ----
837
+
838
+ export interface BestTimeSlot {
839
+ day_of_week: number; // 0=Sunday, 6=Saturday
840
+ day_name: string;
841
+ hour: number;
842
+ avg_engagement: number;
843
+ post_count: number;
844
+ }
845
+
846
+ export interface BestTimeResult {
847
+ best_hours: BestTimeSlot[];
848
+ best_days: { day_of_week: number; day_name: string; avg_engagement: number; post_count: number }[];
849
+ total_analyzed: number;
850
+ }
851
+
852
+ const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
853
+
854
+ /**
855
+ * Analyze historical engagement data to find best time to post
856
+ */
857
+ export function getBestTimeToPost(accountId: string): BestTimeResult {
858
+ const db = getDatabase();
859
+ const rows = db.prepare(
860
+ "SELECT published_at, engagement FROM posts WHERE account_id = ? AND status = 'published' AND published_at IS NOT NULL"
861
+ ).all(accountId) as { published_at: string; engagement: string }[];
862
+
863
+ if (rows.length === 0) {
864
+ return { best_hours: [], best_days: [], total_analyzed: 0 };
865
+ }
866
+
867
+ // Group by day of week and hour
868
+ const hourBuckets: Record<string, { total_engagement: number; count: number; day: number; hour: number }> = {};
869
+ const dayBuckets: Record<number, { total_engagement: number; count: number }> = {};
870
+
871
+ for (const row of rows) {
872
+ const date = new Date(row.published_at.replace(" ", "T"));
873
+ const dayOfWeek = date.getUTCDay();
874
+ const hour = date.getUTCHours();
875
+ const engagement = JSON.parse(row.engagement || "{}") as Engagement;
876
+ const totalEng = (engagement.likes || 0) + (engagement.shares || 0) * 2 +
877
+ (engagement.comments || 0) * 3 + (engagement.clicks || 0);
878
+
879
+ const key = `${dayOfWeek}-${hour}`;
880
+ if (!hourBuckets[key]) {
881
+ hourBuckets[key] = { total_engagement: 0, count: 0, day: dayOfWeek, hour };
882
+ }
883
+ hourBuckets[key].total_engagement += totalEng;
884
+ hourBuckets[key].count += 1;
885
+
886
+ if (!dayBuckets[dayOfWeek]) {
887
+ dayBuckets[dayOfWeek] = { total_engagement: 0, count: 0 };
888
+ }
889
+ dayBuckets[dayOfWeek].total_engagement += totalEng;
890
+ dayBuckets[dayOfWeek].count += 1;
891
+ }
892
+
893
+ // Sort hours by avg engagement
894
+ const best_hours: BestTimeSlot[] = Object.values(hourBuckets)
895
+ .map((b) => ({
896
+ day_of_week: b.day,
897
+ day_name: DAY_NAMES[b.day],
898
+ hour: b.hour,
899
+ avg_engagement: b.count > 0 ? Math.round(b.total_engagement / b.count) : 0,
900
+ post_count: b.count,
901
+ }))
902
+ .sort((a, b) => b.avg_engagement - a.avg_engagement)
903
+ .slice(0, 10);
904
+
905
+ // Sort days by avg engagement
906
+ const best_days = Object.entries(dayBuckets)
907
+ .map(([day, b]) => ({
908
+ day_of_week: parseInt(day),
909
+ day_name: DAY_NAMES[parseInt(day)],
910
+ avg_engagement: b.count > 0 ? Math.round(b.total_engagement / b.count) : 0,
911
+ post_count: b.count,
912
+ }))
913
+ .sort((a, b) => b.avg_engagement - a.avg_engagement);
914
+
915
+ return { best_hours, best_days, total_analyzed: rows.length };
916
+ }
917
+
918
+ // ---- Reschedule ----
919
+
920
+ /**
921
+ * Update scheduled_at on a post without delete/recreate
922
+ */
923
+ export function reschedulePost(id: string, newDate: string): Post | null {
924
+ const post = getPost(id);
925
+ if (!post) return null;
926
+
927
+ if (post.status !== "scheduled" && post.status !== "draft") {
928
+ throw new Error(`Cannot reschedule post with status '${post.status}'. Must be 'draft' or 'scheduled'.`);
929
+ }
930
+
931
+ return updatePost(id, { scheduled_at: newDate, status: "scheduled" });
932
+ }
933
+
934
+ // ---- Approval Workflow ----
935
+
936
+ /**
937
+ * Submit a draft post for review — moves draft→pending_review
938
+ */
939
+ export function submitPostForReview(id: string): Post | null {
940
+ const post = getPost(id);
941
+ if (!post) return null;
942
+
943
+ if (post.status !== "draft") {
944
+ throw new Error(`Cannot submit post with status '${post.status}'. Must be 'draft'.`);
945
+ }
946
+
947
+ return updatePost(id, { status: "pending_review" });
948
+ }
949
+
950
+ /**
951
+ * Approve a post — moves pending_review→scheduled (requires scheduled_at)
952
+ */
953
+ export function approvePost(id: string, scheduledAt?: string): Post | null {
954
+ const post = getPost(id);
955
+ if (!post) return null;
956
+
957
+ if (post.status !== "pending_review") {
958
+ throw new Error(`Cannot approve post with status '${post.status}'. Must be 'pending_review'.`);
959
+ }
960
+
961
+ const targetDate = scheduledAt || post.scheduled_at;
962
+ if (!targetDate) {
963
+ throw new Error("Cannot approve post without a scheduled date. Provide --at <datetime>.");
964
+ }
965
+
966
+ return updatePost(id, { status: "scheduled", scheduled_at: targetDate });
967
+ }
968
+
969
+ // ---- Recurring Posts ----
970
+
971
+ /**
972
+ * Create a recurring post — sets recurrence and schedules the first instance
973
+ */
974
+ export function createRecurringPost(input: CreatePostInput & { recurrence: Recurrence }): Post {
975
+ if (!input.scheduled_at) {
976
+ throw new Error("Recurring posts must have a scheduled_at date for the first occurrence.");
977
+ }
978
+
979
+ return createPost({
980
+ ...input,
981
+ status: "scheduled",
982
+ });
983
+ }
984
+
985
+ // ---- Hashtag Analytics ----
986
+
987
+ export interface HashtagStat {
988
+ hashtag: string;
989
+ post_count: number;
990
+ total_likes: number;
991
+ total_shares: number;
992
+ total_comments: number;
993
+ total_impressions: number;
994
+ avg_engagement: number;
995
+ }
996
+
997
+ /**
998
+ * Extract hashtags from published posts and correlate with engagement
999
+ */
1000
+ export function getHashtagStats(accountId: string): HashtagStat[] {
1001
+ const db = getDatabase();
1002
+ const rows = db.prepare(
1003
+ "SELECT content, engagement FROM posts WHERE account_id = ? AND status = 'published'"
1004
+ ).all(accountId) as { content: string; engagement: string }[];
1005
+
1006
+ const hashtagMap: Record<string, {
1007
+ count: number;
1008
+ likes: number;
1009
+ shares: number;
1010
+ comments: number;
1011
+ impressions: number;
1012
+ }> = {};
1013
+
1014
+ const hashtagRegex = /#(\w+)/g;
1015
+
1016
+ for (const row of rows) {
1017
+ const engagement = JSON.parse(row.engagement || "{}") as Engagement;
1018
+ const matches = row.content.matchAll(hashtagRegex);
1019
+
1020
+ for (const match of matches) {
1021
+ const tag = match[1].toLowerCase();
1022
+ if (!hashtagMap[tag]) {
1023
+ hashtagMap[tag] = { count: 0, likes: 0, shares: 0, comments: 0, impressions: 0 };
1024
+ }
1025
+ hashtagMap[tag].count += 1;
1026
+ hashtagMap[tag].likes += engagement.likes || 0;
1027
+ hashtagMap[tag].shares += engagement.shares || 0;
1028
+ hashtagMap[tag].comments += engagement.comments || 0;
1029
+ hashtagMap[tag].impressions += engagement.impressions || 0;
1030
+ }
1031
+ }
1032
+
1033
+ return Object.entries(hashtagMap)
1034
+ .map(([hashtag, data]) => ({
1035
+ hashtag,
1036
+ post_count: data.count,
1037
+ total_likes: data.likes,
1038
+ total_shares: data.shares,
1039
+ total_comments: data.comments,
1040
+ total_impressions: data.impressions,
1041
+ avg_engagement: data.count > 0
1042
+ ? Math.round((data.likes + data.shares * 2 + data.comments * 3) / data.count)
1043
+ : 0,
1044
+ }))
1045
+ .sort((a, b) => b.avg_engagement - a.avg_engagement);
1046
+ }
@@ -25,10 +25,20 @@ import {
25
25
  getStatsByPlatform,
26
26
  getCalendar,
27
27
  getOverallStats,
28
+ batchSchedule,
29
+ crossPost,
30
+ getBestTimeToPost,
31
+ reschedulePost,
32
+ submitPostForReview,
33
+ approvePost,
34
+ createRecurringPost,
35
+ getHashtagStats,
36
+ PLATFORM_LIMITS,
28
37
  } from "../db/social.js";
29
38
 
30
39
  const PlatformEnum = z.enum(["x", "linkedin", "instagram", "threads", "bluesky"]);
31
- const PostStatusEnum = z.enum(["draft", "scheduled", "published", "failed"]);
40
+ const PostStatusEnum = z.enum(["draft", "scheduled", "published", "failed", "pending_review"]);
41
+ const RecurrenceEnum = z.enum(["daily", "weekly", "biweekly", "monthly"]);
32
42
 
33
43
  const server = new McpServer({
34
44
  name: "microservice-social",
@@ -422,6 +432,216 @@ server.registerTool(
422
432
  }
423
433
  );
424
434
 
435
+ // --- Batch Schedule ---
436
+
437
+ server.registerTool(
438
+ "batch_schedule_posts",
439
+ {
440
+ title: "Batch Schedule Posts",
441
+ description: "Schedule multiple posts at once from an array of post definitions.",
442
+ inputSchema: {
443
+ posts: z.array(z.object({
444
+ account_id: z.string(),
445
+ content: z.string(),
446
+ scheduled_at: z.string(),
447
+ media_urls: z.array(z.string()).optional(),
448
+ tags: z.array(z.string()).optional(),
449
+ recurrence: RecurrenceEnum.optional(),
450
+ })),
451
+ },
452
+ },
453
+ async ({ posts }) => {
454
+ const result = batchSchedule(posts);
455
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
456
+ }
457
+ );
458
+
459
+ // --- Cross-Post ---
460
+
461
+ server.registerTool(
462
+ "crosspost",
463
+ {
464
+ title: "Cross-Post",
465
+ description: "Create identical post on multiple platform accounts.",
466
+ inputSchema: {
467
+ content: z.string(),
468
+ platforms: z.array(PlatformEnum),
469
+ media_urls: z.array(z.string()).optional(),
470
+ tags: z.array(z.string()).optional(),
471
+ scheduled_at: z.string().optional(),
472
+ },
473
+ },
474
+ async ({ content, platforms, ...options }) => {
475
+ try {
476
+ const result = crossPost(content, platforms, options);
477
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
478
+ } catch (error) {
479
+ return {
480
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
481
+ isError: true,
482
+ };
483
+ }
484
+ }
485
+ );
486
+
487
+ // --- Best Time to Post ---
488
+
489
+ server.registerTool(
490
+ "best_time_to_post",
491
+ {
492
+ title: "Best Time to Post",
493
+ description: "Analyze historical engagement to find the best hours and days to post.",
494
+ inputSchema: {
495
+ account_id: z.string(),
496
+ },
497
+ },
498
+ async ({ account_id }) => {
499
+ const result = getBestTimeToPost(account_id);
500
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
501
+ }
502
+ );
503
+
504
+ // --- Reschedule Post ---
505
+
506
+ server.registerTool(
507
+ "reschedule_post",
508
+ {
509
+ title: "Reschedule Post",
510
+ description: "Update the scheduled date/time of a post without delete/recreate.",
511
+ inputSchema: {
512
+ id: z.string(),
513
+ new_date: z.string(),
514
+ },
515
+ },
516
+ async ({ id, new_date }) => {
517
+ try {
518
+ const post = reschedulePost(id, new_date);
519
+ if (!post) {
520
+ return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
521
+ }
522
+ return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
523
+ } catch (error) {
524
+ return {
525
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
526
+ isError: true,
527
+ };
528
+ }
529
+ }
530
+ );
531
+
532
+ // --- Approval Workflow ---
533
+
534
+ server.registerTool(
535
+ "submit_post_for_review",
536
+ {
537
+ title: "Submit Post for Review",
538
+ description: "Submit a draft post for review (draft → pending_review).",
539
+ inputSchema: {
540
+ id: z.string(),
541
+ },
542
+ },
543
+ async ({ id }) => {
544
+ try {
545
+ const post = submitPostForReview(id);
546
+ if (!post) {
547
+ return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
548
+ }
549
+ return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
550
+ } catch (error) {
551
+ return {
552
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
553
+ isError: true,
554
+ };
555
+ }
556
+ }
557
+ );
558
+
559
+ server.registerTool(
560
+ "approve_post",
561
+ {
562
+ title: "Approve Post",
563
+ description: "Approve a post pending review (pending_review → scheduled).",
564
+ inputSchema: {
565
+ id: z.string(),
566
+ scheduled_at: z.string().optional(),
567
+ },
568
+ },
569
+ async ({ id, scheduled_at }) => {
570
+ try {
571
+ const post = approvePost(id, scheduled_at);
572
+ if (!post) {
573
+ return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
574
+ }
575
+ return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
576
+ } catch (error) {
577
+ return {
578
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
579
+ isError: true,
580
+ };
581
+ }
582
+ }
583
+ );
584
+
585
+ // --- Platform Limits ---
586
+
587
+ server.registerTool(
588
+ "get_platform_limits",
589
+ {
590
+ title: "Get Platform Limits",
591
+ description: "Get character limits for all supported platforms.",
592
+ inputSchema: {},
593
+ },
594
+ async () => {
595
+ return { content: [{ type: "text", text: JSON.stringify(PLATFORM_LIMITS, null, 2) }] };
596
+ }
597
+ );
598
+
599
+ // --- Recurring Posts ---
600
+
601
+ server.registerTool(
602
+ "create_recurring_post",
603
+ {
604
+ title: "Create Recurring Post",
605
+ description: "Create a post that recurs on a schedule (daily, weekly, biweekly, monthly).",
606
+ inputSchema: {
607
+ account_id: z.string(),
608
+ content: z.string(),
609
+ scheduled_at: z.string(),
610
+ recurrence: RecurrenceEnum,
611
+ media_urls: z.array(z.string()).optional(),
612
+ tags: z.array(z.string()).optional(),
613
+ },
614
+ },
615
+ async (params) => {
616
+ try {
617
+ const post = createRecurringPost(params);
618
+ return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
619
+ } catch (error) {
620
+ return {
621
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
622
+ isError: true,
623
+ };
624
+ }
625
+ }
626
+ );
627
+
628
+ // --- Hashtag Analytics ---
629
+
630
+ server.registerTool(
631
+ "hashtag_analytics",
632
+ {
633
+ title: "Hashtag Analytics",
634
+ description: "Extract hashtags from published posts and correlate with engagement metrics.",
635
+ inputSchema: {
636
+ account_id: z.string(),
637
+ },
638
+ },
639
+ async ({ account_id }) => {
640
+ const stats = getHashtagStats(account_id);
641
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
642
+ }
643
+ );
644
+
425
645
  // --- Start ---
426
646
  async function main() {
427
647
  const transport = new StdioServerTransport();