@followr/mcp 0.1.0 → 0.2.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 +170 -23
- package/dist/bin/followr-mcp.js +1073 -122
- package/dist/bin/followr-mcp.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/followr-mcp.js
CHANGED
|
@@ -220,9 +220,13 @@ var FollowrClient = class {
|
|
|
220
220
|
// Tags (CRUD complete, sessions 5 + 6 verified empirically)
|
|
221
221
|
// ──────────────────────────────────────────────────────────
|
|
222
222
|
async listTags(companyId, options) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
const query = {
|
|
224
|
+
"filter[company_id]": companyId,
|
|
225
|
+
"page[size]": options?.pageSize ?? 100
|
|
226
|
+
};
|
|
227
|
+
if (options?.name !== void 0)
|
|
228
|
+
query["filter[name]"] = options.name;
|
|
229
|
+
const result = await this.request("GET", "/api/tags", { query });
|
|
226
230
|
return result.data;
|
|
227
231
|
}
|
|
228
232
|
async createTag(body) {
|
|
@@ -1664,9 +1668,890 @@ function registerPostGroupTools(server2, client2, options) {
|
|
|
1664
1668
|
});
|
|
1665
1669
|
}
|
|
1666
1670
|
|
|
1667
|
-
// ../mcp-core/dist/tools/
|
|
1671
|
+
// ../mcp-core/dist/tools/posts.js
|
|
1668
1672
|
import { z as z9 } from "zod";
|
|
1669
|
-
|
|
1673
|
+
|
|
1674
|
+
// ../mcp-core/dist/specs/runtime-context.js
|
|
1675
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
1676
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1677
|
+
async function gatherRuntimeContext(companyId, network, client2) {
|
|
1678
|
+
if (network !== "twitter" && network !== "tiktok")
|
|
1679
|
+
return {};
|
|
1680
|
+
const key = `${companyId}:${network}`;
|
|
1681
|
+
const cached = cache.get(key);
|
|
1682
|
+
if (cached && cached.expiresAt > Date.now())
|
|
1683
|
+
return cached.ctx;
|
|
1684
|
+
let ctx = {};
|
|
1685
|
+
try {
|
|
1686
|
+
const accounts = await client2.listSocialNetworks(companyId);
|
|
1687
|
+
const account = accounts.find((a) => a.type === network);
|
|
1688
|
+
if (account)
|
|
1689
|
+
ctx = extractContext(network, account);
|
|
1690
|
+
} catch {
|
|
1691
|
+
ctx = {};
|
|
1692
|
+
}
|
|
1693
|
+
cache.set(key, { ctx, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
1694
|
+
return ctx;
|
|
1695
|
+
}
|
|
1696
|
+
function extractContext(network, account) {
|
|
1697
|
+
const p = account.preferences ?? {};
|
|
1698
|
+
const ctx = {};
|
|
1699
|
+
if (network === "twitter") {
|
|
1700
|
+
if (typeof p["verified"] === "boolean")
|
|
1701
|
+
ctx.twitter_verified = p["verified"];
|
|
1702
|
+
return ctx;
|
|
1703
|
+
}
|
|
1704
|
+
if (network === "tiktok") {
|
|
1705
|
+
if (typeof p["max_video_post_duration_sec"] === "number") {
|
|
1706
|
+
ctx.tiktok_max_duration_seconds = p["max_video_post_duration_sec"];
|
|
1707
|
+
}
|
|
1708
|
+
if (Array.isArray(p["privacy_level_options"])) {
|
|
1709
|
+
ctx.tiktok_privacy_level_options = p["privacy_level_options"].filter((v) => typeof v === "string");
|
|
1710
|
+
}
|
|
1711
|
+
if (typeof p["duet_disabled"] === "boolean")
|
|
1712
|
+
ctx.tiktok_duet_disabled = p["duet_disabled"];
|
|
1713
|
+
if (typeof p["stitch_disabled"] === "boolean")
|
|
1714
|
+
ctx.tiktok_stitch_disabled = p["stitch_disabled"];
|
|
1715
|
+
if (typeof p["comment_disabled"] === "boolean") {
|
|
1716
|
+
ctx.tiktok_comment_disabled = p["comment_disabled"];
|
|
1717
|
+
}
|
|
1718
|
+
return ctx;
|
|
1719
|
+
}
|
|
1720
|
+
return ctx;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// ../mcp-core/dist/data/social-network-specs.json
|
|
1724
|
+
var social_network_specs_default = {
|
|
1725
|
+
_meta: {
|
|
1726
|
+
source: "Extracted from app.followr.ai frontend bundle, position 1553329-1559200",
|
|
1727
|
+
verified_at: "2026-05-16",
|
|
1728
|
+
extraction_method: "Chrome inspection of webpackChunkfollow main bundle",
|
|
1729
|
+
structure_note: "Top-level keys are `<network>_<product_type>`. Booleans normalized from !0/!1. void 0 \u2192 null. Conditionals noted as both branches where present."
|
|
1730
|
+
},
|
|
1731
|
+
medium_feed: {
|
|
1732
|
+
connection: {
|
|
1733
|
+
required: true
|
|
1734
|
+
},
|
|
1735
|
+
title: {
|
|
1736
|
+
max_length: 100,
|
|
1737
|
+
required: true
|
|
1738
|
+
},
|
|
1739
|
+
description: {
|
|
1740
|
+
max_length: 1024e4,
|
|
1741
|
+
required: true
|
|
1742
|
+
}
|
|
1743
|
+
},
|
|
1744
|
+
pinterest_feed: {
|
|
1745
|
+
connection: {
|
|
1746
|
+
required: true
|
|
1747
|
+
},
|
|
1748
|
+
title: {
|
|
1749
|
+
max_length: 100,
|
|
1750
|
+
required: true
|
|
1751
|
+
},
|
|
1752
|
+
board_id: {
|
|
1753
|
+
required: true
|
|
1754
|
+
},
|
|
1755
|
+
description: {
|
|
1756
|
+
max_length: 500
|
|
1757
|
+
},
|
|
1758
|
+
media_urls: {
|
|
1759
|
+
allow_multiple_types: false,
|
|
1760
|
+
max_images_length: 5,
|
|
1761
|
+
max_videos_length: 1,
|
|
1762
|
+
max_gifs_length: 0,
|
|
1763
|
+
max_length: 5,
|
|
1764
|
+
required: true,
|
|
1765
|
+
image_max_size_bytes: 33554432,
|
|
1766
|
+
video_max_size_bytes: 2097152e3,
|
|
1767
|
+
video_min_duration_seconds: 3,
|
|
1768
|
+
video_max_duration_seconds: 900,
|
|
1769
|
+
same_ratio: true,
|
|
1770
|
+
allow_file_cover: true,
|
|
1771
|
+
allow_frame_cover: true,
|
|
1772
|
+
required_cover: true
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
twitter_feed: {
|
|
1776
|
+
connection: {
|
|
1777
|
+
required: true
|
|
1778
|
+
},
|
|
1779
|
+
description: {
|
|
1780
|
+
max_length: 280,
|
|
1781
|
+
max_length_if_verified: 25e3,
|
|
1782
|
+
_note: "Followr resolves this at runtime: 25000 if the connected Twitter account.preferences.verified is true, else 280"
|
|
1783
|
+
},
|
|
1784
|
+
media_urls: {
|
|
1785
|
+
allow_multiple_types: false,
|
|
1786
|
+
max_images_length: 4,
|
|
1787
|
+
max_videos_length: 1,
|
|
1788
|
+
max_gifs_length: 1,
|
|
1789
|
+
max_length: 4,
|
|
1790
|
+
required: false,
|
|
1791
|
+
image_max_size_bytes: 5242880,
|
|
1792
|
+
video_max_size_bytes: 536870912,
|
|
1793
|
+
video_min_duration_seconds: 0.5,
|
|
1794
|
+
video_max_duration_seconds: 140
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
facebook_feed: {
|
|
1798
|
+
connection: {
|
|
1799
|
+
required: true
|
|
1800
|
+
},
|
|
1801
|
+
description: {
|
|
1802
|
+
max_length: 6e4
|
|
1803
|
+
},
|
|
1804
|
+
media_urls: {
|
|
1805
|
+
allow_multiple_types: false,
|
|
1806
|
+
max_images_length: 10,
|
|
1807
|
+
max_videos_length: 1,
|
|
1808
|
+
max_gifs_length: 0,
|
|
1809
|
+
max_length: 10,
|
|
1810
|
+
required: false,
|
|
1811
|
+
image_max_size_bytes: 8388608,
|
|
1812
|
+
video_max_size_bytes: 10737418240,
|
|
1813
|
+
video_min_duration_seconds: 3,
|
|
1814
|
+
video_max_duration_seconds: 1200,
|
|
1815
|
+
allow_file_cover: false,
|
|
1816
|
+
allow_frame_cover: false
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
facebook_reel: {
|
|
1820
|
+
connection: {
|
|
1821
|
+
required: true
|
|
1822
|
+
},
|
|
1823
|
+
description: {
|
|
1824
|
+
max_length: 6e4
|
|
1825
|
+
},
|
|
1826
|
+
media_urls: {
|
|
1827
|
+
allow_multiple_types: false,
|
|
1828
|
+
max_images_length: 0,
|
|
1829
|
+
max_videos_length: 1,
|
|
1830
|
+
max_gifs_length: 0,
|
|
1831
|
+
max_length: 1,
|
|
1832
|
+
required: true,
|
|
1833
|
+
image_max_size_bytes: 8388608,
|
|
1834
|
+
video_max_size_bytes: 1073741824,
|
|
1835
|
+
video_min_duration_seconds: 3,
|
|
1836
|
+
video_max_duration_seconds: 90,
|
|
1837
|
+
video_max_width: 1920,
|
|
1838
|
+
video_min_width: 540,
|
|
1839
|
+
allowed_aspect_ratios: [
|
|
1840
|
+
0.5,
|
|
1841
|
+
0.6
|
|
1842
|
+
],
|
|
1843
|
+
allow_file_cover: false,
|
|
1844
|
+
allow_frame_cover: false
|
|
1845
|
+
}
|
|
1846
|
+
},
|
|
1847
|
+
facebook_story: {
|
|
1848
|
+
connection: {
|
|
1849
|
+
required: true
|
|
1850
|
+
},
|
|
1851
|
+
description: {
|
|
1852
|
+
max_length: 0
|
|
1853
|
+
},
|
|
1854
|
+
media_urls: {
|
|
1855
|
+
allow_multiple_types: true,
|
|
1856
|
+
max_images_length: 1,
|
|
1857
|
+
max_videos_length: 1,
|
|
1858
|
+
max_gifs_length: 0,
|
|
1859
|
+
max_length: 1,
|
|
1860
|
+
required: true,
|
|
1861
|
+
image_max_size_bytes: 8388608,
|
|
1862
|
+
video_max_size_bytes: 104857600,
|
|
1863
|
+
video_min_duration_seconds: 3,
|
|
1864
|
+
video_max_duration_seconds: 60,
|
|
1865
|
+
video_max_width: 1920,
|
|
1866
|
+
video_min_width: 540,
|
|
1867
|
+
allowed_aspect_ratios: [
|
|
1868
|
+
0.5,
|
|
1869
|
+
0.6
|
|
1870
|
+
]
|
|
1871
|
+
}
|
|
1872
|
+
},
|
|
1873
|
+
instagram_feed: {
|
|
1874
|
+
connection: {
|
|
1875
|
+
required: true
|
|
1876
|
+
},
|
|
1877
|
+
description: {
|
|
1878
|
+
max_length: 2200
|
|
1879
|
+
},
|
|
1880
|
+
media_urls: {
|
|
1881
|
+
allow_multiple_types: false,
|
|
1882
|
+
max_images_length: 10,
|
|
1883
|
+
max_videos_length: 1,
|
|
1884
|
+
max_gifs_length: 0,
|
|
1885
|
+
max_length: 10,
|
|
1886
|
+
required: true,
|
|
1887
|
+
image_max_size_bytes: 8388608,
|
|
1888
|
+
video_max_size_bytes: 10737418240,
|
|
1889
|
+
video_min_duration_seconds: 3,
|
|
1890
|
+
video_max_duration_seconds: 900,
|
|
1891
|
+
video_max_width: 1920,
|
|
1892
|
+
allowed_aspect_ratios: [
|
|
1893
|
+
0.5,
|
|
1894
|
+
1.91
|
|
1895
|
+
],
|
|
1896
|
+
allow_file_cover: true,
|
|
1897
|
+
allow_frame_cover: true
|
|
1898
|
+
}
|
|
1899
|
+
},
|
|
1900
|
+
instagram_reel: {
|
|
1901
|
+
connection: {
|
|
1902
|
+
required: true
|
|
1903
|
+
},
|
|
1904
|
+
description: {
|
|
1905
|
+
max_length: 2200
|
|
1906
|
+
},
|
|
1907
|
+
media_urls: {
|
|
1908
|
+
allow_multiple_types: false,
|
|
1909
|
+
max_images_length: 0,
|
|
1910
|
+
max_videos_length: 1,
|
|
1911
|
+
max_gifs_length: 0,
|
|
1912
|
+
max_length: 1,
|
|
1913
|
+
required: true,
|
|
1914
|
+
image_max_size_bytes: null,
|
|
1915
|
+
video_max_size_bytes: 1073741824,
|
|
1916
|
+
video_min_duration_seconds: 3,
|
|
1917
|
+
video_max_duration_seconds: 900,
|
|
1918
|
+
video_max_width: 1920,
|
|
1919
|
+
video_min_width: 720,
|
|
1920
|
+
allowed_aspect_ratios: [
|
|
1921
|
+
0.5,
|
|
1922
|
+
1.91
|
|
1923
|
+
],
|
|
1924
|
+
allow_file_cover: true,
|
|
1925
|
+
allow_frame_cover: true
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
instagram_story: {
|
|
1929
|
+
connection: {
|
|
1930
|
+
required: true
|
|
1931
|
+
},
|
|
1932
|
+
description: {
|
|
1933
|
+
max_length: 0
|
|
1934
|
+
},
|
|
1935
|
+
media_urls: {
|
|
1936
|
+
allow_multiple_types: true,
|
|
1937
|
+
max_images_length: 1,
|
|
1938
|
+
max_videos_length: 1,
|
|
1939
|
+
max_gifs_length: 0,
|
|
1940
|
+
max_length: 1,
|
|
1941
|
+
required: true,
|
|
1942
|
+
image_max_size_bytes: 8388608,
|
|
1943
|
+
video_max_size_bytes: 104857600,
|
|
1944
|
+
video_min_duration_seconds: 3,
|
|
1945
|
+
video_max_duration_seconds: 60,
|
|
1946
|
+
video_max_width: 1920,
|
|
1947
|
+
video_min_width: 540,
|
|
1948
|
+
allowed_aspect_ratios: [
|
|
1949
|
+
0.5,
|
|
1950
|
+
0.6
|
|
1951
|
+
],
|
|
1952
|
+
allow_file_cover: false,
|
|
1953
|
+
allow_frame_cover: false
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
tiktok_feed: {
|
|
1957
|
+
connection: {
|
|
1958
|
+
required: true
|
|
1959
|
+
},
|
|
1960
|
+
description: {
|
|
1961
|
+
max_length: 2200,
|
|
1962
|
+
required: false
|
|
1963
|
+
},
|
|
1964
|
+
media_urls: {
|
|
1965
|
+
allow_multiple_types: false,
|
|
1966
|
+
max_images_length: 0,
|
|
1967
|
+
max_videos_length: 1,
|
|
1968
|
+
max_gifs_length: 0,
|
|
1969
|
+
max_length: 1,
|
|
1970
|
+
required: true,
|
|
1971
|
+
image_max_size_bytes: null,
|
|
1972
|
+
video_max_size_bytes: 4294967296,
|
|
1973
|
+
video_min_duration_seconds: 3,
|
|
1974
|
+
video_max_duration_seconds: 600,
|
|
1975
|
+
allow_file_cover: false,
|
|
1976
|
+
allow_frame_cover: true,
|
|
1977
|
+
_video_max_duration_note: "Followr resolves this at runtime: if connected TikTok account.video_duration is set, uses that; otherwise falls back to 600s. Max varies per TikTok account (1-3-10-60 min tiers)"
|
|
1978
|
+
},
|
|
1979
|
+
privacy_level: {
|
|
1980
|
+
required: true
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
linkedin_feed: {
|
|
1984
|
+
connection: {
|
|
1985
|
+
required: true
|
|
1986
|
+
},
|
|
1987
|
+
description: {
|
|
1988
|
+
max_length: 3e3,
|
|
1989
|
+
required: true
|
|
1990
|
+
},
|
|
1991
|
+
media_urls: {
|
|
1992
|
+
allow_multiple_types: false,
|
|
1993
|
+
max_images_length: 9,
|
|
1994
|
+
max_videos_length: 1,
|
|
1995
|
+
max_gifs_length: 0,
|
|
1996
|
+
max_length: 9,
|
|
1997
|
+
required: false,
|
|
1998
|
+
image_max_size_bytes: 8388608,
|
|
1999
|
+
video_max_size_bytes: 209715200,
|
|
2000
|
+
video_min_duration_seconds: 3,
|
|
2001
|
+
video_max_duration_seconds: 600
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
youtube_feed: {
|
|
2005
|
+
connection: {
|
|
2006
|
+
required: true
|
|
2007
|
+
},
|
|
2008
|
+
title: {
|
|
2009
|
+
max_length: 100,
|
|
2010
|
+
required: true
|
|
2011
|
+
},
|
|
2012
|
+
description: {
|
|
2013
|
+
max_length: 5e3,
|
|
2014
|
+
required: false
|
|
2015
|
+
},
|
|
2016
|
+
media_urls: {
|
|
2017
|
+
allow_multiple_types: false,
|
|
2018
|
+
max_images_length: 0,
|
|
2019
|
+
max_videos_length: 1,
|
|
2020
|
+
max_gifs_length: 0,
|
|
2021
|
+
max_length: 1,
|
|
2022
|
+
required: true,
|
|
2023
|
+
image_max_size_bytes: null,
|
|
2024
|
+
video_max_size_bytes: 137438953472,
|
|
2025
|
+
video_min_duration_seconds: 1,
|
|
2026
|
+
video_max_duration_seconds: 43200,
|
|
2027
|
+
video_max_width: 7680,
|
|
2028
|
+
video_min_width: 426,
|
|
2029
|
+
allowed_aspect_ratios: [
|
|
2030
|
+
0.3333,
|
|
2031
|
+
2.3922
|
|
2032
|
+
],
|
|
2033
|
+
allow_file_cover: true,
|
|
2034
|
+
allow_frame_cover: true
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
youtube_short: {
|
|
2038
|
+
connection: {
|
|
2039
|
+
required: true
|
|
2040
|
+
},
|
|
2041
|
+
title: {
|
|
2042
|
+
max_length: 100,
|
|
2043
|
+
required: false
|
|
2044
|
+
},
|
|
2045
|
+
description: {
|
|
2046
|
+
max_length: 5e3,
|
|
2047
|
+
required: false
|
|
2048
|
+
},
|
|
2049
|
+
media_urls: {
|
|
2050
|
+
allow_multiple_types: false,
|
|
2051
|
+
max_images_length: 0,
|
|
2052
|
+
max_videos_length: 1,
|
|
2053
|
+
max_gifs_length: 0,
|
|
2054
|
+
max_length: 1,
|
|
2055
|
+
required: true,
|
|
2056
|
+
video_max_size_bytes: 268435456,
|
|
2057
|
+
video_min_duration_seconds: 1,
|
|
2058
|
+
video_max_duration_seconds: 60,
|
|
2059
|
+
video_max_width: 1920,
|
|
2060
|
+
video_min_width: 600,
|
|
2061
|
+
allowed_aspect_ratios: [
|
|
2062
|
+
0.5,
|
|
2063
|
+
0.625
|
|
2064
|
+
],
|
|
2065
|
+
allow_file_cover: true,
|
|
2066
|
+
allow_frame_cover: true
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
threads_feed: {
|
|
2070
|
+
connection: {
|
|
2071
|
+
required: true
|
|
2072
|
+
},
|
|
2073
|
+
description: {
|
|
2074
|
+
max_length: 500
|
|
2075
|
+
},
|
|
2076
|
+
media_urls: {
|
|
2077
|
+
allow_multiple_types: true,
|
|
2078
|
+
max_images_length: 20,
|
|
2079
|
+
max_videos_length: 20,
|
|
2080
|
+
max_gifs_length: 20,
|
|
2081
|
+
max_length: 20,
|
|
2082
|
+
required: false,
|
|
2083
|
+
image_max_size_bytes: 5242880,
|
|
2084
|
+
video_max_size_bytes: 536870912,
|
|
2085
|
+
video_min_duration_seconds: 0.5,
|
|
2086
|
+
video_max_duration_seconds: 140
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
bluesky_feed: {
|
|
2090
|
+
connection: {
|
|
2091
|
+
required: true
|
|
2092
|
+
},
|
|
2093
|
+
description: {
|
|
2094
|
+
max_length: 300
|
|
2095
|
+
},
|
|
2096
|
+
media_urls: {
|
|
2097
|
+
allow_multiple_types: false,
|
|
2098
|
+
max_images_length: 4,
|
|
2099
|
+
max_videos_length: 1,
|
|
2100
|
+
max_gifs_length: 0,
|
|
2101
|
+
max_length: 4,
|
|
2102
|
+
required: false,
|
|
2103
|
+
image_max_size_bytes: 1048576,
|
|
2104
|
+
video_max_size_bytes: 52428800,
|
|
2105
|
+
video_min_duration_seconds: 1,
|
|
2106
|
+
video_max_duration_seconds: 180
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
// ../mcp-core/dist/specs/loader.js
|
|
2112
|
+
var REGISTRY = social_network_specs_default;
|
|
2113
|
+
function getSpec(network, productType) {
|
|
2114
|
+
const key = `${network}_${productType}`;
|
|
2115
|
+
return REGISTRY[key] ?? null;
|
|
2116
|
+
}
|
|
2117
|
+
function getSpecsMeta() {
|
|
2118
|
+
return REGISTRY._meta;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// ../mcp-core/dist/specs/validate.js
|
|
2122
|
+
function validateAgainstSpec(payload, context = {}) {
|
|
2123
|
+
const spec = getSpec(payload.network, payload.product_type);
|
|
2124
|
+
if (!spec)
|
|
2125
|
+
return [];
|
|
2126
|
+
const specKey = `${payload.network}_${payload.product_type}`;
|
|
2127
|
+
const warnings = [];
|
|
2128
|
+
warnings.push(...validateTitle(spec, payload, specKey));
|
|
2129
|
+
warnings.push(...validateDescription(spec, payload, specKey, context));
|
|
2130
|
+
warnings.push(...validateBoardId(spec, payload, specKey));
|
|
2131
|
+
warnings.push(...validatePrivacyLevel(spec, payload, specKey, context));
|
|
2132
|
+
warnings.push(...validateMedia(spec, payload, specKey, context));
|
|
2133
|
+
return warnings;
|
|
2134
|
+
}
|
|
2135
|
+
function validateTitle(spec, payload, specKey) {
|
|
2136
|
+
const rule = spec.title;
|
|
2137
|
+
if (!rule)
|
|
2138
|
+
return [];
|
|
2139
|
+
const warnings = [];
|
|
2140
|
+
const title = payload.title?.trim() ?? "";
|
|
2141
|
+
if (rule.required && title.length === 0) {
|
|
2142
|
+
warnings.push({
|
|
2143
|
+
spec_key: specKey,
|
|
2144
|
+
field: "title",
|
|
2145
|
+
rule: "required",
|
|
2146
|
+
current_value: payload.title ?? null,
|
|
2147
|
+
expected: "non-empty string",
|
|
2148
|
+
severity: "hard_fail",
|
|
2149
|
+
suggestion: `Title is required for ${payload.network} ${payload.product_type}.`
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
if (rule.max_length != null && title.length > rule.max_length) {
|
|
2153
|
+
warnings.push({
|
|
2154
|
+
spec_key: specKey,
|
|
2155
|
+
field: "title.max_length",
|
|
2156
|
+
rule: "max_length_exceeded",
|
|
2157
|
+
current_value: title.length,
|
|
2158
|
+
expected: rule.max_length,
|
|
2159
|
+
severity: "hard_fail",
|
|
2160
|
+
suggestion: `Trim title to ${rule.max_length} characters or less. Currently ${title.length}.`
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
return warnings;
|
|
2164
|
+
}
|
|
2165
|
+
function validateDescription(spec, payload, specKey, context) {
|
|
2166
|
+
const rule = spec.description;
|
|
2167
|
+
if (!rule)
|
|
2168
|
+
return [];
|
|
2169
|
+
const warnings = [];
|
|
2170
|
+
const description = payload.description ?? "";
|
|
2171
|
+
const descTrimmed = description.trim();
|
|
2172
|
+
if (rule.required && descTrimmed.length === 0) {
|
|
2173
|
+
warnings.push({
|
|
2174
|
+
spec_key: specKey,
|
|
2175
|
+
field: "description",
|
|
2176
|
+
rule: "required",
|
|
2177
|
+
current_value: payload.description ?? null,
|
|
2178
|
+
expected: "non-empty string",
|
|
2179
|
+
severity: "hard_fail",
|
|
2180
|
+
suggestion: `Description is required for ${payload.network} ${payload.product_type}.`
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
let effectiveMax = rule.max_length;
|
|
2184
|
+
let bumpedByVerified = false;
|
|
2185
|
+
if (payload.network === "twitter" && context.twitter_verified === true && rule.max_length_if_verified != null) {
|
|
2186
|
+
effectiveMax = rule.max_length_if_verified;
|
|
2187
|
+
bumpedByVerified = true;
|
|
2188
|
+
}
|
|
2189
|
+
if (effectiveMax == null)
|
|
2190
|
+
return warnings;
|
|
2191
|
+
if (effectiveMax === 0 && description.length > 0) {
|
|
2192
|
+
warnings.push({
|
|
2193
|
+
spec_key: specKey,
|
|
2194
|
+
field: "description",
|
|
2195
|
+
rule: "not_supported",
|
|
2196
|
+
current_value: description.length,
|
|
2197
|
+
expected: 0,
|
|
2198
|
+
severity: "hard_fail",
|
|
2199
|
+
suggestion: `${payload.network} ${payload.product_type} doesn't support a caption. The description field is ignored.`
|
|
2200
|
+
});
|
|
2201
|
+
return warnings;
|
|
2202
|
+
}
|
|
2203
|
+
if (description.length > effectiveMax) {
|
|
2204
|
+
let verifiedNote = "";
|
|
2205
|
+
if (payload.network === "twitter" && !context.twitter_verified && rule.max_length_if_verified != null) {
|
|
2206
|
+
verifiedNote = ` (Twitter Premium subscribers get up to ${rule.max_length_if_verified}.)`;
|
|
2207
|
+
}
|
|
2208
|
+
warnings.push({
|
|
2209
|
+
spec_key: specKey,
|
|
2210
|
+
field: "description.max_length",
|
|
2211
|
+
rule: "max_length_exceeded",
|
|
2212
|
+
current_value: description.length,
|
|
2213
|
+
expected: effectiveMax,
|
|
2214
|
+
severity: "hard_fail",
|
|
2215
|
+
suggestion: `Trim caption to ${effectiveMax} characters or less. Currently ${description.length}.${verifiedNote}`
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
if (!bumpedByVerified && payload.network === "twitter" && rule.max_length_if_verified != null && description.length > rule.max_length && description.length <= rule.max_length_if_verified) {
|
|
2219
|
+
}
|
|
2220
|
+
return warnings;
|
|
2221
|
+
}
|
|
2222
|
+
function validateBoardId(spec, payload, specKey) {
|
|
2223
|
+
if (!spec.board_id?.required)
|
|
2224
|
+
return [];
|
|
2225
|
+
const boardId = payload.preferences?.["board_id"];
|
|
2226
|
+
if (boardId != null && boardId !== "")
|
|
2227
|
+
return [];
|
|
2228
|
+
return [
|
|
2229
|
+
{
|
|
2230
|
+
spec_key: specKey,
|
|
2231
|
+
field: "preferences.board_id",
|
|
2232
|
+
rule: "required",
|
|
2233
|
+
current_value: boardId ?? null,
|
|
2234
|
+
expected: "Pinterest board id (number)",
|
|
2235
|
+
severity: "hard_fail",
|
|
2236
|
+
suggestion: "Pinterest requires a destination board. Set preferences.board_id to a valid board id."
|
|
2237
|
+
}
|
|
2238
|
+
];
|
|
2239
|
+
}
|
|
2240
|
+
function validatePrivacyLevel(spec, payload, specKey, context) {
|
|
2241
|
+
if (!spec.privacy_level?.required)
|
|
2242
|
+
return [];
|
|
2243
|
+
const warnings = [];
|
|
2244
|
+
const prefs = payload.preferences;
|
|
2245
|
+
const level = prefs?.["privacy_level"];
|
|
2246
|
+
if (level == null || typeof level !== "string" || level === "") {
|
|
2247
|
+
warnings.push({
|
|
2248
|
+
spec_key: specKey,
|
|
2249
|
+
field: "preferences.privacy_level",
|
|
2250
|
+
rule: "required",
|
|
2251
|
+
current_value: level ?? null,
|
|
2252
|
+
expected: context.tiktok_privacy_level_options ?? "one of TikTok's privacy levels",
|
|
2253
|
+
severity: "hard_fail",
|
|
2254
|
+
suggestion: `TikTok requires preferences.privacy_level. ${context.tiktok_privacy_level_options ? `Allowed for this account: ${context.tiktok_privacy_level_options.join(", ")}.` : "Check the connected TikTok account for allowed values."}`
|
|
2255
|
+
});
|
|
2256
|
+
return warnings;
|
|
2257
|
+
}
|
|
2258
|
+
if (context.tiktok_privacy_level_options && !context.tiktok_privacy_level_options.includes(level)) {
|
|
2259
|
+
warnings.push({
|
|
2260
|
+
spec_key: specKey,
|
|
2261
|
+
field: "preferences.privacy_level",
|
|
2262
|
+
rule: "value_not_allowed",
|
|
2263
|
+
current_value: level,
|
|
2264
|
+
expected: context.tiktok_privacy_level_options,
|
|
2265
|
+
severity: "hard_fail",
|
|
2266
|
+
suggestion: `Privacy level "${level}" is not enabled for this TikTok account. Allowed: ${context.tiktok_privacy_level_options.join(", ")}.`
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
return warnings;
|
|
2270
|
+
}
|
|
2271
|
+
function validateMedia(spec, payload, specKey, context) {
|
|
2272
|
+
const m = spec.media_urls;
|
|
2273
|
+
if (!m)
|
|
2274
|
+
return [];
|
|
2275
|
+
const warnings = [];
|
|
2276
|
+
const assets = payload.assets ?? [];
|
|
2277
|
+
if (m.required && assets.length === 0) {
|
|
2278
|
+
warnings.push({
|
|
2279
|
+
spec_key: specKey,
|
|
2280
|
+
field: "assets",
|
|
2281
|
+
rule: "required",
|
|
2282
|
+
current_value: 0,
|
|
2283
|
+
expected: ">=1 asset",
|
|
2284
|
+
severity: "hard_fail",
|
|
2285
|
+
suggestion: `${payload.network} ${payload.product_type} requires at least one media asset.`
|
|
2286
|
+
});
|
|
2287
|
+
return warnings;
|
|
2288
|
+
}
|
|
2289
|
+
if (assets.length === 0)
|
|
2290
|
+
return warnings;
|
|
2291
|
+
if (m.allow_multiple_types === false) {
|
|
2292
|
+
const types = new Set(assets.map((a) => a.type));
|
|
2293
|
+
if (types.size > 1) {
|
|
2294
|
+
warnings.push({
|
|
2295
|
+
spec_key: specKey,
|
|
2296
|
+
field: "assets.types",
|
|
2297
|
+
rule: "no_mixed_media",
|
|
2298
|
+
current_value: Array.from(types),
|
|
2299
|
+
expected: "single media type",
|
|
2300
|
+
severity: "hard_fail",
|
|
2301
|
+
suggestion: `${payload.network} ${payload.product_type} doesn't allow mixed media types in one post. Use either all images, all videos, or all GIFs.`
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
const counts = {
|
|
2306
|
+
image: assets.filter((a) => a.type === "image").length,
|
|
2307
|
+
video: assets.filter((a) => a.type === "video").length,
|
|
2308
|
+
gif: assets.filter((a) => a.type === "gif").length
|
|
2309
|
+
};
|
|
2310
|
+
if (m.max_images_length != null && counts.image > m.max_images_length) {
|
|
2311
|
+
warnings.push(countWarning(specKey, payload, "images", counts.image, m.max_images_length));
|
|
2312
|
+
}
|
|
2313
|
+
if (m.max_videos_length != null && counts.video > m.max_videos_length) {
|
|
2314
|
+
warnings.push(countWarning(specKey, payload, "videos", counts.video, m.max_videos_length));
|
|
2315
|
+
}
|
|
2316
|
+
if (m.max_gifs_length != null && counts.gif > m.max_gifs_length) {
|
|
2317
|
+
warnings.push(countWarning(specKey, payload, "gifs", counts.gif, m.max_gifs_length));
|
|
2318
|
+
}
|
|
2319
|
+
if (m.max_length != null && assets.length > m.max_length) {
|
|
2320
|
+
warnings.push({
|
|
2321
|
+
spec_key: specKey,
|
|
2322
|
+
field: "assets",
|
|
2323
|
+
rule: "max_total_exceeded",
|
|
2324
|
+
current_value: assets.length,
|
|
2325
|
+
expected: m.max_length,
|
|
2326
|
+
severity: "hard_fail",
|
|
2327
|
+
suggestion: `${payload.network} ${payload.product_type} accepts up to ${m.max_length} total assets. You attached ${assets.length}.`
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
const effectiveVideoMaxDuration = resolveVideoMaxDuration(payload, context, m);
|
|
2331
|
+
for (let i = 0; i < assets.length; i++) {
|
|
2332
|
+
warnings.push(...validateAsset(assets[i], i, specKey, payload, m, effectiveVideoMaxDuration));
|
|
2333
|
+
}
|
|
2334
|
+
return warnings;
|
|
2335
|
+
}
|
|
2336
|
+
function resolveVideoMaxDuration(payload, context, m) {
|
|
2337
|
+
if (payload.network === "tiktok" && context.tiktok_max_duration_seconds != null) {
|
|
2338
|
+
return context.tiktok_max_duration_seconds;
|
|
2339
|
+
}
|
|
2340
|
+
return m.video_max_duration_seconds;
|
|
2341
|
+
}
|
|
2342
|
+
function validateAsset(asset, index, specKey, payload, m, effectiveVideoMaxDuration) {
|
|
2343
|
+
const warnings = [];
|
|
2344
|
+
const prefix = `assets[${index}]`;
|
|
2345
|
+
if (asset.type === "image" && m.image_max_size_bytes != null && asset.size_bytes != null && asset.size_bytes > m.image_max_size_bytes) {
|
|
2346
|
+
warnings.push({
|
|
2347
|
+
spec_key: specKey,
|
|
2348
|
+
field: `${prefix}.size_bytes`,
|
|
2349
|
+
rule: "max_size_exceeded",
|
|
2350
|
+
current_value: asset.size_bytes,
|
|
2351
|
+
expected: m.image_max_size_bytes,
|
|
2352
|
+
severity: "hard_fail",
|
|
2353
|
+
suggestion: `Image at index ${index} is ${formatBytes(asset.size_bytes)}, max is ${formatBytes(m.image_max_size_bytes)} for ${payload.network} ${payload.product_type}.`
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
if (asset.type === "video" && m.video_max_size_bytes != null && asset.size_bytes != null && asset.size_bytes > m.video_max_size_bytes) {
|
|
2357
|
+
warnings.push({
|
|
2358
|
+
spec_key: specKey,
|
|
2359
|
+
field: `${prefix}.size_bytes`,
|
|
2360
|
+
rule: "max_size_exceeded",
|
|
2361
|
+
current_value: asset.size_bytes,
|
|
2362
|
+
expected: m.video_max_size_bytes,
|
|
2363
|
+
severity: "hard_fail",
|
|
2364
|
+
suggestion: `Video at index ${index} is ${formatBytes(asset.size_bytes)}, max is ${formatBytes(m.video_max_size_bytes)} for ${payload.network} ${payload.product_type}.`
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
if (asset.type === "video" && asset.duration_seconds != null) {
|
|
2368
|
+
if (effectiveVideoMaxDuration != null && asset.duration_seconds > effectiveVideoMaxDuration) {
|
|
2369
|
+
warnings.push({
|
|
2370
|
+
spec_key: specKey,
|
|
2371
|
+
field: `${prefix}.duration_seconds`,
|
|
2372
|
+
rule: "video_too_long",
|
|
2373
|
+
current_value: asset.duration_seconds,
|
|
2374
|
+
expected: effectiveVideoMaxDuration,
|
|
2375
|
+
severity: "hard_fail",
|
|
2376
|
+
suggestion: `Video at index ${index} is ${asset.duration_seconds}s, max is ${effectiveVideoMaxDuration}s for ${payload.network} ${payload.product_type}.`
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
if (m.video_min_duration_seconds != null && asset.duration_seconds < m.video_min_duration_seconds) {
|
|
2380
|
+
warnings.push({
|
|
2381
|
+
spec_key: specKey,
|
|
2382
|
+
field: `${prefix}.duration_seconds`,
|
|
2383
|
+
rule: "video_too_short",
|
|
2384
|
+
current_value: asset.duration_seconds,
|
|
2385
|
+
expected: m.video_min_duration_seconds,
|
|
2386
|
+
severity: "hard_fail",
|
|
2387
|
+
suggestion: `Video at index ${index} is ${asset.duration_seconds}s, min is ${m.video_min_duration_seconds}s for ${payload.network} ${payload.product_type}.`
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (asset.type === "video" && asset.width != null) {
|
|
2392
|
+
if (m.video_max_width != null && asset.width > m.video_max_width) {
|
|
2393
|
+
warnings.push({
|
|
2394
|
+
spec_key: specKey,
|
|
2395
|
+
field: `${prefix}.width`,
|
|
2396
|
+
rule: "video_max_width_exceeded",
|
|
2397
|
+
current_value: asset.width,
|
|
2398
|
+
expected: m.video_max_width,
|
|
2399
|
+
severity: "hard_fail",
|
|
2400
|
+
suggestion: `Video at index ${index} is ${asset.width}px wide, max is ${m.video_max_width}px.`
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
if (m.video_min_width != null && asset.width < m.video_min_width) {
|
|
2404
|
+
warnings.push({
|
|
2405
|
+
spec_key: specKey,
|
|
2406
|
+
field: `${prefix}.width`,
|
|
2407
|
+
rule: "video_min_width_below",
|
|
2408
|
+
current_value: asset.width,
|
|
2409
|
+
expected: m.video_min_width,
|
|
2410
|
+
severity: "hard_fail",
|
|
2411
|
+
suggestion: `Video at index ${index} is ${asset.width}px wide, min is ${m.video_min_width}px.`
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
if (m.allowed_aspect_ratios && asset.width != null && asset.height != null && asset.height > 0) {
|
|
2416
|
+
const ratio = asset.width / asset.height;
|
|
2417
|
+
const [min, max] = m.allowed_aspect_ratios;
|
|
2418
|
+
if (ratio < min || ratio > max) {
|
|
2419
|
+
warnings.push({
|
|
2420
|
+
spec_key: specKey,
|
|
2421
|
+
field: `${prefix}.aspect_ratio`,
|
|
2422
|
+
rule: "aspect_ratio_out_of_range",
|
|
2423
|
+
current_value: round(ratio, 3),
|
|
2424
|
+
expected: m.allowed_aspect_ratios,
|
|
2425
|
+
severity: "hard_fail",
|
|
2426
|
+
suggestion: `Asset at index ${index} has aspect ratio ${round(ratio, 3)} (${asset.width}\xD7${asset.height}). ${payload.network} ${payload.product_type} accepts [${min}, ${max}].`
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return warnings;
|
|
2431
|
+
}
|
|
2432
|
+
function countWarning(specKey, payload, kind, actual, max) {
|
|
2433
|
+
return {
|
|
2434
|
+
spec_key: specKey,
|
|
2435
|
+
field: `assets.${kind}`,
|
|
2436
|
+
rule: "max_count_exceeded",
|
|
2437
|
+
current_value: actual,
|
|
2438
|
+
expected: max,
|
|
2439
|
+
severity: "hard_fail",
|
|
2440
|
+
suggestion: `${payload.network} ${payload.product_type} accepts up to ${max} ${kind === "videos" && max === 1 ? "video" : kind === "images" && max === 1 ? "image" : kind === "gifs" && max === 1 ? "GIF" : kind}. You attached ${actual}.`
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function formatBytes(bytes) {
|
|
2444
|
+
if (bytes >= 1024 ** 3)
|
|
2445
|
+
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
2446
|
+
if (bytes >= 1024 ** 2)
|
|
2447
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
2448
|
+
if (bytes >= 1024)
|
|
2449
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2450
|
+
return `${bytes} B`;
|
|
2451
|
+
}
|
|
2452
|
+
function round(n, decimals) {
|
|
2453
|
+
const factor = 10 ** decimals;
|
|
2454
|
+
return Math.round(n * factor) / factor;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ../mcp-core/dist/tools/posts.js
|
|
2458
|
+
var NETWORK_ENUM = [
|
|
2459
|
+
"medium",
|
|
2460
|
+
"pinterest",
|
|
2461
|
+
"twitter",
|
|
2462
|
+
"facebook",
|
|
2463
|
+
"instagram",
|
|
2464
|
+
"tiktok",
|
|
2465
|
+
"linkedin",
|
|
2466
|
+
"youtube",
|
|
2467
|
+
"threads",
|
|
2468
|
+
"bluesky"
|
|
2469
|
+
];
|
|
2470
|
+
var PRODUCT_TYPE_ENUM = ["feed", "reel", "story", "short"];
|
|
2471
|
+
var NETWORKS_NEEDING_MEDIA_PRODUCT_TYPE = /* @__PURE__ */ new Set([
|
|
2472
|
+
"instagram",
|
|
2473
|
+
"facebook",
|
|
2474
|
+
"youtube"
|
|
2475
|
+
]);
|
|
2476
|
+
var PRODUCT_TYPE_TO_FOLLOWR = {
|
|
2477
|
+
feed: "FEED",
|
|
2478
|
+
reel: "REEL",
|
|
2479
|
+
story: "STORY",
|
|
2480
|
+
short: "SHORT"
|
|
2481
|
+
};
|
|
2482
|
+
var AssetInputSchema = z9.object({
|
|
2483
|
+
id: z9.number().int().positive().describe("Followr asset id (returned by upload_image_from_url / upload_video_from_url / list_assets)."),
|
|
2484
|
+
type: z9.enum(["image", "video", "gif"]).describe("Asset type. Required for spec validation."),
|
|
2485
|
+
width: z9.number().positive().optional().describe("Optional. If provided, used for aspect ratio and width checks."),
|
|
2486
|
+
height: z9.number().positive().optional(),
|
|
2487
|
+
size_bytes: z9.number().nonnegative().optional().describe("Optional. If provided, used for file size checks."),
|
|
2488
|
+
duration_seconds: z9.number().positive().optional().describe("Optional, video assets only. Used for duration checks.")
|
|
2489
|
+
});
|
|
2490
|
+
function registerPostTools(server2, client2, _options) {
|
|
2491
|
+
server2.registerTool("create_post", {
|
|
2492
|
+
title: "Create a Post inside a PostGroup, attaching assets and copy",
|
|
2493
|
+
description: "Create a Post (per-network entry) within an existing PostGroup. This is the second step of the manual PostGroup \u2192 Post \u2192 Schedule workflow: after create_post_group, call create_post once per target social network, then update_post_group to set publish_at. Pre-validates the payload against per-network specs (caption length, asset count/type/size/aspect ratio, etc.) and returns advisory warnings alongside the created post. Warnings are informational \u2014 the post is always created. The caller (LLM or user) decides whether to act on warnings before scheduling.",
|
|
2494
|
+
inputSchema: {
|
|
2495
|
+
post_group_id: z9.number().int().positive().describe("Parent PostGroup id from create_post_group."),
|
|
2496
|
+
company_id: z9.number().int().positive().describe("Followr workspace id. Required to resolve account-specific limits (Twitter verified, TikTok tier) for validation."),
|
|
2497
|
+
social_network_type: z9.enum(NETWORK_ENUM).describe("Target social network."),
|
|
2498
|
+
product_type: z9.enum(PRODUCT_TYPE_ENUM).describe("Post format. 'feed' = standard feed post. 'reel' = vertical short-form video (IG Reels, FB Reels). 'story' = ephemeral 9:16 (IG/FB Stories). 'short' = YouTube Shorts. For IG/FB/YouTube, this is auto-injected as preferences.media_product_type in the Followr API body."),
|
|
2499
|
+
description: z9.string().optional().describe("Caption / body text."),
|
|
2500
|
+
title: z9.string().optional().describe("Title (medium / pinterest / youtube only)."),
|
|
2501
|
+
link: z9.string().optional().describe("Optional outbound link."),
|
|
2502
|
+
assets: z9.array(AssetInputSchema).optional().describe("Media to attach. Asset ids are extracted into the API's assets_ids array. The full metadata (type, width, height, size, duration) is used for validation only \u2014 not sent to the API."),
|
|
2503
|
+
preferences: z9.record(z9.string(), z9.unknown()).optional().describe("Network-specific extras passed through to Followr. e.g. board_id (Pinterest), privacy_level + duet_disabled (TikTok), category_id (YouTube), notify_followers (IG). media_product_type is auto-injected from product_type for IG/FB/YouTube if not already set."),
|
|
2504
|
+
comments_to_create: z9.array(z9.unknown()).optional().describe("First-comment payloads (e.g., for IG hashtag dumping in the first comment).")
|
|
2505
|
+
}
|
|
2506
|
+
}, async (input) => {
|
|
2507
|
+
const context = await gatherRuntimeContext(input.company_id, input.social_network_type, client2);
|
|
2508
|
+
const mergedPreferences = { ...input.preferences ?? {} };
|
|
2509
|
+
if (NETWORKS_NEEDING_MEDIA_PRODUCT_TYPE.has(input.social_network_type) && !("media_product_type" in mergedPreferences)) {
|
|
2510
|
+
mergedPreferences["media_product_type"] = PRODUCT_TYPE_TO_FOLLOWR[input.product_type];
|
|
2511
|
+
}
|
|
2512
|
+
const warnings = validateAgainstSpec({
|
|
2513
|
+
network: input.social_network_type,
|
|
2514
|
+
product_type: input.product_type,
|
|
2515
|
+
description: input.description,
|
|
2516
|
+
title: input.title,
|
|
2517
|
+
link: input.link,
|
|
2518
|
+
assets: input.assets,
|
|
2519
|
+
preferences: mergedPreferences
|
|
2520
|
+
}, context);
|
|
2521
|
+
const body = {
|
|
2522
|
+
social_network_type: input.social_network_type
|
|
2523
|
+
};
|
|
2524
|
+
if (input.description !== void 0)
|
|
2525
|
+
body.description = input.description;
|
|
2526
|
+
if (input.title !== void 0)
|
|
2527
|
+
body.title = input.title;
|
|
2528
|
+
if (input.link !== void 0)
|
|
2529
|
+
body.link = input.link;
|
|
2530
|
+
if (input.assets && input.assets.length > 0) {
|
|
2531
|
+
body.assets_ids = input.assets.map((a) => a.id);
|
|
2532
|
+
}
|
|
2533
|
+
if (Object.keys(mergedPreferences).length > 0)
|
|
2534
|
+
body.preferences = mergedPreferences;
|
|
2535
|
+
if (input.comments_to_create !== void 0)
|
|
2536
|
+
body.comments_to_create = input.comments_to_create;
|
|
2537
|
+
const post = await client2.createPost(input.post_group_id, body);
|
|
2538
|
+
const response = {
|
|
2539
|
+
post,
|
|
2540
|
+
validation: {
|
|
2541
|
+
warning_count: warnings.length,
|
|
2542
|
+
warnings,
|
|
2543
|
+
runtime_context: context
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
return {
|
|
2547
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
2548
|
+
};
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// ../mcp-core/dist/tools/prompts.js
|
|
2553
|
+
import { z as z10 } from "zod";
|
|
2554
|
+
var SOCIAL_NETWORK_TYPE = z10.enum([
|
|
1670
2555
|
"facebook",
|
|
1671
2556
|
"twitter",
|
|
1672
2557
|
"instagram",
|
|
@@ -1683,10 +2568,10 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1683
2568
|
title: "List brand-voice prompts for a workspace",
|
|
1684
2569
|
description: "List the brand-voice prompts of a workspace. These are the per-network prompt templates that Followr picks among when generating content (Post Generator, etc.). Optional filter by social network type and by `default=true`. Pass company_id=0 to see Followr's built-in defaults (where company_id is null in the API).",
|
|
1685
2570
|
inputSchema: {
|
|
1686
|
-
company_id:
|
|
2571
|
+
company_id: z10.number().int().nonnegative().describe("Workspace id. Pass 0 to query Followr's built-in defaults (the API maps 0 \u2192 null)."),
|
|
1687
2572
|
social_network_type: SOCIAL_NETWORK_TYPE.optional().describe("Restrict to one network."),
|
|
1688
|
-
only_default:
|
|
1689
|
-
page_size:
|
|
2573
|
+
only_default: z10.boolean().optional().describe("If true, only return prompts marked default."),
|
|
2574
|
+
page_size: z10.number().int().positive().max(100).optional()
|
|
1690
2575
|
}
|
|
1691
2576
|
}, async ({ company_id, social_network_type, only_default, page_size }) => {
|
|
1692
2577
|
const prompts = await client2.listPrompts({
|
|
@@ -1716,7 +2601,7 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1716
2601
|
title: "Get a single brand-voice prompt by id",
|
|
1717
2602
|
description: "Fetch one brand-voice prompt by id. Useful to inspect its current text, default flag, and network assignment.",
|
|
1718
2603
|
inputSchema: {
|
|
1719
|
-
prompt_id:
|
|
2604
|
+
prompt_id: z10.number().int().positive()
|
|
1720
2605
|
}
|
|
1721
2606
|
}, async ({ prompt_id }) => {
|
|
1722
2607
|
const prompt = await client2.getPrompt(prompt_id);
|
|
@@ -1726,12 +2611,12 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1726
2611
|
title: "Create a brand-voice prompt for a workspace",
|
|
1727
2612
|
description: "Create a custom brand-voice prompt attached to a workspace and a specific social network. Followr will consider this prompt (alongside any built-in defaults and other workspace prompts) when generating content. Mark `default=true` to make it eligible for automatic selection. Multiple prompts with default=true per network are allowed; Followr picks one at generate time.",
|
|
1728
2613
|
inputSchema: {
|
|
1729
|
-
company_id:
|
|
2614
|
+
company_id: z10.number().int().positive(),
|
|
1730
2615
|
social_network_type: SOCIAL_NETWORK_TYPE,
|
|
1731
|
-
name:
|
|
1732
|
-
prompt:
|
|
1733
|
-
default:
|
|
1734
|
-
type:
|
|
2616
|
+
name: z10.string().min(1).max(80).describe("Short human-readable name shown in the Followr UI."),
|
|
2617
|
+
prompt: z10.string().min(1).describe("The actual prompt text used as system instructions when generating."),
|
|
2618
|
+
default: z10.boolean().optional().describe("If true, marks this prompt as eligible for automatic selection. Default false."),
|
|
2619
|
+
type: z10.string().optional().describe("Resource type. Default `text` (the only verified value). Reserved for future image/video prompt types.")
|
|
1735
2620
|
}
|
|
1736
2621
|
}, async ({ company_id, social_network_type, name, prompt, default: isDefault, type }) => {
|
|
1737
2622
|
const created = await client2.createPrompt({
|
|
@@ -1748,11 +2633,11 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1748
2633
|
title: "Update a brand-voice prompt",
|
|
1749
2634
|
description: "Patch an existing brand-voice prompt. Use to rename, edit the prompt text, change the network assignment, or toggle the `default` flag.",
|
|
1750
2635
|
inputSchema: {
|
|
1751
|
-
prompt_id:
|
|
1752
|
-
name:
|
|
1753
|
-
prompt:
|
|
2636
|
+
prompt_id: z10.number().int().positive(),
|
|
2637
|
+
name: z10.string().min(1).max(80).optional(),
|
|
2638
|
+
prompt: z10.string().min(1).optional(),
|
|
1754
2639
|
social_network_type: SOCIAL_NETWORK_TYPE.optional(),
|
|
1755
|
-
default:
|
|
2640
|
+
default: z10.boolean().optional()
|
|
1756
2641
|
}
|
|
1757
2642
|
}, async ({ prompt_id, ...patch }) => {
|
|
1758
2643
|
const updated = await client2.updatePrompt(prompt_id, patch);
|
|
@@ -1762,7 +2647,7 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1762
2647
|
title: "Delete a brand-voice prompt (destructive)",
|
|
1763
2648
|
description: "Permanently delete a brand-voice prompt. Cannot be undone. Followr's built-in defaults (where company_id is null) cannot be deleted; only workspace-scoped prompts can.",
|
|
1764
2649
|
inputSchema: {
|
|
1765
|
-
prompt_id:
|
|
2650
|
+
prompt_id: z10.number().int().positive()
|
|
1766
2651
|
}
|
|
1767
2652
|
}, async ({ prompt_id }) => {
|
|
1768
2653
|
await client2.deletePrompt(prompt_id);
|
|
@@ -1771,14 +2656,14 @@ function registerPromptTools(server2, client2, _options) {
|
|
|
1771
2656
|
}
|
|
1772
2657
|
|
|
1773
2658
|
// ../mcp-core/dist/tools/rule-groups.js
|
|
1774
|
-
import { z as
|
|
2659
|
+
import { z as z11 } from "zod";
|
|
1775
2660
|
function registerRuleGroupTools(server2, client2, _options) {
|
|
1776
2661
|
server2.registerTool("list_rule_groups", {
|
|
1777
2662
|
title: "List Autopilot rule groups in a workspace",
|
|
1778
2663
|
description: "List rule groups (Autopilot scheduling rules) in a workspace. A rule group bundles rules that auto-fill empty calendar slots from a pool of tagged PostGroups. Includes the underlying rules array by default.",
|
|
1779
2664
|
inputSchema: {
|
|
1780
|
-
company_id:
|
|
1781
|
-
include:
|
|
2665
|
+
company_id: z11.number().int().positive(),
|
|
2666
|
+
include: z11.string().optional().describe("Override include chain. Default: rules.")
|
|
1782
2667
|
}
|
|
1783
2668
|
}, async ({ company_id, include }) => {
|
|
1784
2669
|
const groups = await client2.listRuleGroups(company_id, {
|
|
@@ -1805,7 +2690,7 @@ function registerRuleGroupTools(server2, client2, _options) {
|
|
|
1805
2690
|
title: "Get a single rule group by id",
|
|
1806
2691
|
description: "Fetch one Autopilot rule group's details. Path is flat (/api/ruleGroups/{id}).",
|
|
1807
2692
|
inputSchema: {
|
|
1808
|
-
rule_group_id:
|
|
2693
|
+
rule_group_id: z11.number().int().positive()
|
|
1809
2694
|
}
|
|
1810
2695
|
}, async ({ rule_group_id }) => {
|
|
1811
2696
|
const group = await client2.getRuleGroup(rule_group_id);
|
|
@@ -1815,11 +2700,11 @@ function registerRuleGroupTools(server2, client2, _options) {
|
|
|
1815
2700
|
title: "Create an Autopilot rule group",
|
|
1816
2701
|
description: "Create a new Autopilot rule group in a workspace. After creation, individual rules (days_of_week, time_slots, social_network_types, tag filters) must be added separately. random_minutes adds jitter to scheduled times to avoid bot-like patterns.",
|
|
1817
2702
|
inputSchema: {
|
|
1818
|
-
company_id:
|
|
1819
|
-
name:
|
|
1820
|
-
description:
|
|
1821
|
-
is_active:
|
|
1822
|
-
random_minutes:
|
|
2703
|
+
company_id: z11.number().int().positive(),
|
|
2704
|
+
name: z11.string().min(1),
|
|
2705
|
+
description: z11.string().optional(),
|
|
2706
|
+
is_active: z11.boolean().optional().describe("If true, the rule group is active immediately. Default false."),
|
|
2707
|
+
random_minutes: z11.number().int().min(0).max(120).optional().describe("Random jitter in minutes applied to scheduled times. Default 0.")
|
|
1823
2708
|
}
|
|
1824
2709
|
}, async ({ company_id, name, description, is_active, random_minutes }) => {
|
|
1825
2710
|
const group = await client2.createRuleGroup({
|
|
@@ -1835,11 +2720,11 @@ function registerRuleGroupTools(server2, client2, _options) {
|
|
|
1835
2720
|
title: "Update an Autopilot rule group",
|
|
1836
2721
|
description: "Patch a rule group's name, description, active flag, or random jitter.",
|
|
1837
2722
|
inputSchema: {
|
|
1838
|
-
rule_group_id:
|
|
1839
|
-
name:
|
|
1840
|
-
description:
|
|
1841
|
-
active:
|
|
1842
|
-
random_minutes:
|
|
2723
|
+
rule_group_id: z11.number().int().positive(),
|
|
2724
|
+
name: z11.string().min(1).optional(),
|
|
2725
|
+
description: z11.string().optional(),
|
|
2726
|
+
active: z11.boolean().optional(),
|
|
2727
|
+
random_minutes: z11.number().int().min(0).max(120).optional()
|
|
1843
2728
|
}
|
|
1844
2729
|
}, async ({ rule_group_id, ...patch }) => {
|
|
1845
2730
|
const group = await client2.updateRuleGroup(rule_group_id, patch);
|
|
@@ -1849,7 +2734,7 @@ function registerRuleGroupTools(server2, client2, _options) {
|
|
|
1849
2734
|
title: "Delete an Autopilot rule group (destructive)",
|
|
1850
2735
|
description: "Permanently delete an Autopilot rule group. Already-scheduled posts that were filled by this group will remain on the calendar (deletion only affects future autopilot fills). Cannot be undone.",
|
|
1851
2736
|
inputSchema: {
|
|
1852
|
-
rule_group_id:
|
|
2737
|
+
rule_group_id: z11.number().int().positive()
|
|
1853
2738
|
}
|
|
1854
2739
|
}, async ({ rule_group_id }) => {
|
|
1855
2740
|
await client2.deleteRuleGroup(rule_group_id);
|
|
@@ -1858,16 +2743,16 @@ function registerRuleGroupTools(server2, client2, _options) {
|
|
|
1858
2743
|
}
|
|
1859
2744
|
|
|
1860
2745
|
// ../mcp-core/dist/tools/social-hub.js
|
|
1861
|
-
import { z as
|
|
2746
|
+
import { z as z12 } from "zod";
|
|
1862
2747
|
function registerSocialHubTools(server2, client2, _options) {
|
|
1863
2748
|
server2.registerTool("list_conversations", {
|
|
1864
2749
|
title: "List inbox conversations (DMs) for a workspace",
|
|
1865
2750
|
description: "List Social Hub conversations across all connected accounts in a workspace, newest activity first. Each entry includes the external user (DM sender), last message preview, and unread count. Use this to triage the inbox or auto-reply.",
|
|
1866
2751
|
inputSchema: {
|
|
1867
|
-
company_id:
|
|
1868
|
-
social_network_id:
|
|
1869
|
-
only_unread:
|
|
1870
|
-
page_size:
|
|
2752
|
+
company_id: z12.number().int().positive(),
|
|
2753
|
+
social_network_id: z12.number().int().positive().optional().describe("Optional: limit to a specific connected account."),
|
|
2754
|
+
only_unread: z12.boolean().optional().describe("If true, only return conversations with unread messages."),
|
|
2755
|
+
page_size: z12.number().int().positive().max(100).optional()
|
|
1871
2756
|
}
|
|
1872
2757
|
}, async ({ company_id, social_network_id, only_unread, page_size }) => {
|
|
1873
2758
|
const conversations = await client2.listConversations(company_id, {
|
|
@@ -1901,8 +2786,8 @@ function registerSocialHubTools(server2, client2, _options) {
|
|
|
1901
2786
|
title: "Get messages in a conversation",
|
|
1902
2787
|
description: "Return the messages within a single conversation, newest first. Use this after list_conversations to read full thread content before deciding on a reply.",
|
|
1903
2788
|
inputSchema: {
|
|
1904
|
-
conversation_id:
|
|
1905
|
-
page_size:
|
|
2789
|
+
conversation_id: z12.number().int().positive(),
|
|
2790
|
+
page_size: z12.number().int().positive().max(100).optional()
|
|
1906
2791
|
}
|
|
1907
2792
|
}, async ({ conversation_id, page_size }) => {
|
|
1908
2793
|
const messages = await client2.listMessages(conversation_id, {
|
|
@@ -1921,7 +2806,7 @@ function registerSocialHubTools(server2, client2, _options) {
|
|
|
1921
2806
|
title: "Mark a conversation as read",
|
|
1922
2807
|
description: "Mark a Social Hub conversation as read (clears the unread badge). Use this after processing messages from get_conversation_messages.",
|
|
1923
2808
|
inputSchema: {
|
|
1924
|
-
conversation_id:
|
|
2809
|
+
conversation_id: z12.number().int().positive()
|
|
1925
2810
|
}
|
|
1926
2811
|
}, async ({ conversation_id }) => {
|
|
1927
2812
|
const updated = await client2.markConversationRead(conversation_id);
|
|
@@ -1931,8 +2816,8 @@ function registerSocialHubTools(server2, client2, _options) {
|
|
|
1931
2816
|
title: "List messages via the platform-native endpoint (Facebook or Instagram only)",
|
|
1932
2817
|
description: "Read messages using Followr's platform-specific proxy to the Meta Graph API. Only supported for Facebook and Instagram conversations (other networks like LinkedIn, TikTok, X, Threads, Bluesky return 404 from this proxy and should use get_conversation_messages instead). Returns the raw Graph API shape with created_time, from, id, message, to fields.",
|
|
1933
2818
|
inputSchema: {
|
|
1934
|
-
platform:
|
|
1935
|
-
conversation_id:
|
|
2819
|
+
platform: z12.enum(["facebook", "instagram"]).describe("Only facebook and instagram are supported."),
|
|
2820
|
+
conversation_id: z12.number().int().positive()
|
|
1936
2821
|
}
|
|
1937
2822
|
}, async ({ platform, conversation_id }) => {
|
|
1938
2823
|
const messages = await client2.listPlatformMessages(platform, conversation_id);
|
|
@@ -1942,9 +2827,9 @@ function registerSocialHubTools(server2, client2, _options) {
|
|
|
1942
2827
|
title: "List external users (contacts) in a workspace",
|
|
1943
2828
|
description: "List external users associated with a workspace. External users are DM senders, followers, or comment authors collected across all connected accounts. Use this to see who has interacted with the brand recently. Internally calls /api/externalUsers (which is the same resource the Followr UI's Contacts page reads from).",
|
|
1944
2829
|
inputSchema: {
|
|
1945
|
-
company_id:
|
|
1946
|
-
type:
|
|
1947
|
-
page_size:
|
|
2830
|
+
company_id: z12.number().int().positive(),
|
|
2831
|
+
type: z12.string().optional().describe("Optional filter by external user type (e.g. follower, message_sender, comment_author)."),
|
|
2832
|
+
page_size: z12.number().int().positive().max(100).optional()
|
|
1948
2833
|
}
|
|
1949
2834
|
}, async ({ company_id, type, page_size }) => {
|
|
1950
2835
|
const contacts = await client2.listExternalUsers(company_id, {
|
|
@@ -1972,10 +2857,10 @@ function registerSocialHubTools(server2, client2, _options) {
|
|
|
1972
2857
|
title: "List comments on published posts in a workspace",
|
|
1973
2858
|
description: "Return comments left on the workspace's published posts. Use this for community-moderation workflows: surface new comments, draft replies, escalate negative sentiment. Optionally narrow to a single post via post_id.",
|
|
1974
2859
|
inputSchema: {
|
|
1975
|
-
company_id:
|
|
1976
|
-
post_id:
|
|
1977
|
-
page_size:
|
|
1978
|
-
include:
|
|
2860
|
+
company_id: z12.number().int().positive(),
|
|
2861
|
+
post_id: z12.number().int().positive().optional().describe("Optional: limit to a single post."),
|
|
2862
|
+
page_size: z12.number().int().positive().max(100).optional(),
|
|
2863
|
+
include: z12.string().optional().describe("Optional include chain (e.g. 'externalUser,post').")
|
|
1979
2864
|
}
|
|
1980
2865
|
}, async ({ company_id, post_id, page_size, include }) => {
|
|
1981
2866
|
const comments = await client2.listComments(company_id, {
|
|
@@ -2000,13 +2885,13 @@ function registerSubscriptionTools(server2, client2, _options) {
|
|
|
2000
2885
|
}
|
|
2001
2886
|
|
|
2002
2887
|
// ../mcp-core/dist/tools/tags.js
|
|
2003
|
-
import { z as
|
|
2888
|
+
import { z as z13 } from "zod";
|
|
2004
2889
|
function registerTagTools(server2, client2, _options) {
|
|
2005
2890
|
server2.registerTool("list_tags", {
|
|
2006
2891
|
title: "List tags in a workspace",
|
|
2007
2892
|
description: "List all tags for a Followr workspace. Tags are scoped to a company and used for categorizing PostGroups (and as a workaround for approval status via the 'Approved' / 'Rejected' convention).",
|
|
2008
2893
|
inputSchema: {
|
|
2009
|
-
company_id:
|
|
2894
|
+
company_id: z13.number().int().positive()
|
|
2010
2895
|
}
|
|
2011
2896
|
}, async ({ company_id }) => {
|
|
2012
2897
|
const tags = await client2.listTags(company_id);
|
|
@@ -2023,9 +2908,9 @@ function registerTagTools(server2, client2, _options) {
|
|
|
2023
2908
|
title: "Create a tag in a workspace",
|
|
2024
2909
|
description: "Create a new tag in the specified workspace. Color is optional hex (e.g. #22c55e). Tags are idempotent by convention: caller should list existing first to avoid duplicates.",
|
|
2025
2910
|
inputSchema: {
|
|
2026
|
-
company_id:
|
|
2027
|
-
name:
|
|
2028
|
-
color:
|
|
2911
|
+
company_id: z13.number().int().positive(),
|
|
2912
|
+
name: z13.string().min(1),
|
|
2913
|
+
color: z13.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color, e.g. #22c55e")
|
|
2029
2914
|
}
|
|
2030
2915
|
}, async ({ company_id, name, color }) => {
|
|
2031
2916
|
const tag = await client2.createTag({ company_id, name, ...color ? { color } : {} });
|
|
@@ -2035,10 +2920,10 @@ function registerTagTools(server2, client2, _options) {
|
|
|
2035
2920
|
title: "Update a tag (rename, change color or active state)",
|
|
2036
2921
|
description: "Patch a tag's name, color, or active flag.",
|
|
2037
2922
|
inputSchema: {
|
|
2038
|
-
tag_id:
|
|
2039
|
-
name:
|
|
2040
|
-
color:
|
|
2041
|
-
active:
|
|
2923
|
+
tag_id: z13.number().int().positive(),
|
|
2924
|
+
name: z13.string().optional(),
|
|
2925
|
+
color: z13.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
2926
|
+
active: z13.boolean().optional()
|
|
2042
2927
|
}
|
|
2043
2928
|
}, async ({ tag_id, ...patch }) => {
|
|
2044
2929
|
const tag = await client2.updateTag(tag_id, patch);
|
|
@@ -2048,7 +2933,7 @@ function registerTagTools(server2, client2, _options) {
|
|
|
2048
2933
|
title: "Delete a tag (destructive)",
|
|
2049
2934
|
description: "Permanently delete a tag. PostGroups that referenced this tag will keep their tags_ids list with a now-broken reference. Cannot be undone.",
|
|
2050
2935
|
inputSchema: {
|
|
2051
|
-
tag_id:
|
|
2936
|
+
tag_id: z13.number().int().positive()
|
|
2052
2937
|
}
|
|
2053
2938
|
}, async ({ tag_id }) => {
|
|
2054
2939
|
await client2.deleteTag(tag_id);
|
|
@@ -2058,13 +2943,13 @@ function registerTagTools(server2, client2, _options) {
|
|
|
2058
2943
|
title: "Find a tag by name or create it if it doesn't exist",
|
|
2059
2944
|
description: "Idempotent helper. Looks up tags in the workspace by case-insensitive name match. If found, returns its id. If not, creates a new tag with the given name and color.",
|
|
2060
2945
|
inputSchema: {
|
|
2061
|
-
company_id:
|
|
2062
|
-
name:
|
|
2063
|
-
color:
|
|
2946
|
+
company_id: z13.number().int().positive(),
|
|
2947
|
+
name: z13.string().min(1),
|
|
2948
|
+
color: z13.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
|
|
2064
2949
|
}
|
|
2065
2950
|
}, async ({ company_id, name, color }) => {
|
|
2066
|
-
const
|
|
2067
|
-
const existing =
|
|
2951
|
+
const matches = await client2.listTags(company_id, { name });
|
|
2952
|
+
const existing = matches.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
2068
2953
|
if (existing) {
|
|
2069
2954
|
return {
|
|
2070
2955
|
content: [{ type: "text", text: JSON.stringify({ found: true, tag: existing }, null, 2) }]
|
|
@@ -2078,7 +2963,7 @@ function registerTagTools(server2, client2, _options) {
|
|
|
2078
2963
|
}
|
|
2079
2964
|
|
|
2080
2965
|
// ../mcp-core/dist/tools/users.js
|
|
2081
|
-
import { z as
|
|
2966
|
+
import { z as z14 } from "zod";
|
|
2082
2967
|
function registerUserTools(server2, client2, _options) {
|
|
2083
2968
|
server2.registerTool("get_current_user", {
|
|
2084
2969
|
title: "Get the user that owns the current API token",
|
|
@@ -2102,8 +2987,8 @@ function registerUserTools(server2, client2, _options) {
|
|
|
2102
2987
|
title: "List users with access to a workspace",
|
|
2103
2988
|
description: "List Followr users that have access to a specific workspace (team members). Uses the relation filter `companies.id`. Use to map ownership, find collaborators, or check who created a draft.",
|
|
2104
2989
|
inputSchema: {
|
|
2105
|
-
company_id:
|
|
2106
|
-
page_size:
|
|
2990
|
+
company_id: z14.number().int().positive(),
|
|
2991
|
+
page_size: z14.number().int().positive().max(100).optional()
|
|
2107
2992
|
}
|
|
2108
2993
|
}, async ({ company_id, page_size }) => {
|
|
2109
2994
|
const users = await client2.listUsersInCompany(company_id, {
|
|
@@ -2126,8 +3011,72 @@ function registerUserTools(server2, client2, _options) {
|
|
|
2126
3011
|
});
|
|
2127
3012
|
}
|
|
2128
3013
|
|
|
3014
|
+
// ../mcp-core/dist/tools/validate.js
|
|
3015
|
+
import { z as z15 } from "zod";
|
|
3016
|
+
var NETWORK_ENUM2 = [
|
|
3017
|
+
"medium",
|
|
3018
|
+
"pinterest",
|
|
3019
|
+
"twitter",
|
|
3020
|
+
"facebook",
|
|
3021
|
+
"instagram",
|
|
3022
|
+
"tiktok",
|
|
3023
|
+
"linkedin",
|
|
3024
|
+
"youtube",
|
|
3025
|
+
"threads",
|
|
3026
|
+
"bluesky"
|
|
3027
|
+
];
|
|
3028
|
+
var PRODUCT_TYPE_ENUM2 = ["feed", "reel", "story", "short"];
|
|
3029
|
+
var AssetSchema = z15.object({
|
|
3030
|
+
id: z15.number().int().positive().optional(),
|
|
3031
|
+
type: z15.enum(["image", "video", "gif"]),
|
|
3032
|
+
width: z15.number().positive().optional(),
|
|
3033
|
+
height: z15.number().positive().optional(),
|
|
3034
|
+
size_bytes: z15.number().nonnegative().optional(),
|
|
3035
|
+
duration_seconds: z15.number().positive().optional()
|
|
3036
|
+
});
|
|
3037
|
+
function registerValidateTools(server2, client2, _options) {
|
|
3038
|
+
server2.registerTool("validate_against_specs", {
|
|
3039
|
+
title: "Validate a Post payload against social network specs (advisory)",
|
|
3040
|
+
description: "Check if a post (caption, assets, preferences) violates any social network rules before publishing. Returns advisory warnings; does NOT block. Use BEFORE expensive operations (generate_avatar_video, image generation, etc.) to avoid wasting credits on outputs the platform will reject. Asset metadata (size_bytes, width, height, duration_seconds) is optional \u2014 provide what you have, the validator skips checks for missing fields.",
|
|
3041
|
+
inputSchema: {
|
|
3042
|
+
company_id: z15.number().int().positive().describe("Followr workspace id. Required to resolve account-specific limits (Twitter verified, TikTok tier)."),
|
|
3043
|
+
network: z15.enum(NETWORK_ENUM2).describe("Target social network."),
|
|
3044
|
+
product_type: z15.enum(PRODUCT_TYPE_ENUM2).describe("Post format. e.g. 'feed' for IG Feed, 'reel' for IG Reels, 'story' for IG Stories, 'short' for YouTube Shorts."),
|
|
3045
|
+
description: z15.string().optional().describe("Post caption / body text."),
|
|
3046
|
+
title: z15.string().optional().describe("Title (medium, pinterest, youtube only)."),
|
|
3047
|
+
link: z15.string().optional().describe("Optional outbound link."),
|
|
3048
|
+
assets: z15.array(AssetSchema).optional().describe("Media to attach. Each asset's metadata is used for size/duration/dimensions/aspect ratio checks."),
|
|
3049
|
+
preferences: z15.record(z15.string(), z15.unknown()).optional().describe("Network-specific extras: board_id (Pinterest), privacy_level (TikTok), category_id (YouTube), media_product_type (FEED/REELS/STORY for IG), etc.")
|
|
3050
|
+
}
|
|
3051
|
+
}, async (input) => {
|
|
3052
|
+
const context = await gatherRuntimeContext(input.company_id, input.network, client2);
|
|
3053
|
+
const spec = getSpec(input.network, input.product_type);
|
|
3054
|
+
const specKey = `${input.network}_${input.product_type}`;
|
|
3055
|
+
const warnings = validateAgainstSpec({
|
|
3056
|
+
network: input.network,
|
|
3057
|
+
product_type: input.product_type,
|
|
3058
|
+
description: input.description,
|
|
3059
|
+
title: input.title,
|
|
3060
|
+
link: input.link,
|
|
3061
|
+
assets: input.assets,
|
|
3062
|
+
preferences: input.preferences
|
|
3063
|
+
}, context);
|
|
3064
|
+
const response = {
|
|
3065
|
+
spec_key: specKey,
|
|
3066
|
+
spec_exists: spec !== null,
|
|
3067
|
+
runtime_context: context,
|
|
3068
|
+
warning_count: warnings.length,
|
|
3069
|
+
warnings,
|
|
3070
|
+
specs_verified_at: getSpecsMeta()?.verified_at ?? null
|
|
3071
|
+
};
|
|
3072
|
+
return {
|
|
3073
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
3074
|
+
};
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
|
|
2129
3078
|
// ../mcp-core/dist/tools/voices.js
|
|
2130
|
-
import { z as
|
|
3079
|
+
import { z as z16 } from "zod";
|
|
2131
3080
|
function sanitizeVoice(voice) {
|
|
2132
3081
|
const v = voice;
|
|
2133
3082
|
if (v.company) {
|
|
@@ -2141,8 +3090,8 @@ function registerVoiceTools(server2, client2, _options) {
|
|
|
2141
3090
|
title: "List voices in a workspace",
|
|
2142
3091
|
description: "List voice profiles already created in a Followr workspace. Each voice is linked to a TTS provider (typically ElevenLabs) and can be assigned to an avatar or used directly for audio generation. Use this before generate_audio or create_avatar_full_flow to discover available voices.",
|
|
2143
3092
|
inputSchema: {
|
|
2144
|
-
company_id:
|
|
2145
|
-
page_size:
|
|
3093
|
+
company_id: z16.number().int().positive(),
|
|
3094
|
+
page_size: z16.number().int().positive().max(100).optional()
|
|
2146
3095
|
}
|
|
2147
3096
|
}, async ({ company_id, page_size }) => {
|
|
2148
3097
|
const voices = await client2.listVoices(company_id, {
|
|
@@ -2169,7 +3118,7 @@ function registerVoiceTools(server2, client2, _options) {
|
|
|
2169
3118
|
title: "Get a single voice with its audio sample",
|
|
2170
3119
|
description: "Fetch one voice by id, with the audio sample hydrated. Use this to confirm a freshly created voice has its sample uploaded, or to retrieve the audio preview URL.",
|
|
2171
3120
|
inputSchema: {
|
|
2172
|
-
voice_id:
|
|
3121
|
+
voice_id: z16.number().int().positive()
|
|
2173
3122
|
}
|
|
2174
3123
|
}, async ({ voice_id }) => {
|
|
2175
3124
|
const voice = await client2.getVoice(voice_id);
|
|
@@ -2179,12 +3128,12 @@ function registerVoiceTools(server2, client2, _options) {
|
|
|
2179
3128
|
title: "Browse the ElevenLabs voice catalog with optional filters",
|
|
2180
3129
|
description: "Returns a page of voices from the ElevenLabs catalog with rich metadata (language, gender, age, accent, use_case, preview_url, social handles). Supports client-side filtering by language, gender, category, and free-text query against name and description. Use this to pick a voice_id before calling create_voice_from_elevenlabs.",
|
|
2181
3130
|
inputSchema: {
|
|
2182
|
-
page:
|
|
2183
|
-
language:
|
|
2184
|
-
gender:
|
|
2185
|
-
category:
|
|
2186
|
-
query:
|
|
2187
|
-
featured_only:
|
|
3131
|
+
page: z16.number().int().positive().optional().describe("API page number. 30 voices per page. Default 1."),
|
|
3132
|
+
language: z16.string().optional().describe("Filter by ISO 639-1 code (en, es, pt, fr, etc.)."),
|
|
3133
|
+
gender: z16.enum(["male", "female", "non-binary"]).optional(),
|
|
3134
|
+
category: z16.string().optional().describe("e.g. professional, casual."),
|
|
3135
|
+
query: z16.string().optional().describe("Substring match against name or description (case-insensitive)."),
|
|
3136
|
+
featured_only: z16.boolean().optional().describe("If true, only return voices marked featured.")
|
|
2188
3137
|
}
|
|
2189
3138
|
}, async ({ page, language, gender, category, query, featured_only }) => {
|
|
2190
3139
|
const all = await client2.listElevenlabsVoices({ ...page ? { page } : {} });
|
|
@@ -2225,12 +3174,12 @@ function registerVoiceTools(server2, client2, _options) {
|
|
|
2225
3174
|
title: "Create a Followr voice linked to an ElevenLabs voice",
|
|
2226
3175
|
description: "Create a Voice resource in the workspace that wraps an ElevenLabs voice_id. Required: name, language_code (ISO 639-1), elevenlabs_voice_id (from list_elevenlabs_voices). The voice is usable immediately for generate_audio and create_avatar_full_flow even though the optional audio sample upload is not part of this tool (voice.audio will be null until manually uploaded). Cannot be undone via the MCP (voice resource persists).",
|
|
2227
3176
|
inputSchema: {
|
|
2228
|
-
company_id:
|
|
2229
|
-
name:
|
|
2230
|
-
language_code:
|
|
2231
|
-
elevenlabs_voice_id:
|
|
2232
|
-
accent:
|
|
2233
|
-
description:
|
|
3177
|
+
company_id: z16.number().int().positive(),
|
|
3178
|
+
name: z16.string().min(1).max(50).describe("Human-readable voice name."),
|
|
3179
|
+
language_code: z16.string().min(2).max(5).describe("ISO 639-1 code, e.g. en, es, pt, fr."),
|
|
3180
|
+
elevenlabs_voice_id: z16.string().min(1).describe("ElevenLabs voice_id from list_elevenlabs_voices."),
|
|
3181
|
+
accent: z16.string().optional(),
|
|
3182
|
+
description: z16.string().optional()
|
|
2234
3183
|
}
|
|
2235
3184
|
}, async ({ company_id, name, language_code, elevenlabs_voice_id, accent, description }) => {
|
|
2236
3185
|
const voice = await client2.createVoice(company_id, {
|
|
@@ -2246,15 +3195,15 @@ function registerVoiceTools(server2, client2, _options) {
|
|
|
2246
3195
|
}
|
|
2247
3196
|
|
|
2248
3197
|
// ../mcp-core/dist/tools/workspace-settings.js
|
|
2249
|
-
import { z as
|
|
3198
|
+
import { z as z17 } from "zod";
|
|
2250
3199
|
function registerWorkspaceSettingsTools(server2, client2, _options) {
|
|
2251
3200
|
server2.registerTool("update_webhook_url", {
|
|
2252
3201
|
title: "Update the workspace webhook URL and secret",
|
|
2253
3202
|
description: "Set or rotate the workspace's outbound webhook (used when a Post is published or fails). Both fields can be cleared with empty string. The secret never round-trips: stored and signed against, never echoed back; the response only confirms the URL.",
|
|
2254
3203
|
inputSchema: {
|
|
2255
|
-
company_id:
|
|
2256
|
-
webhook_posts_url:
|
|
2257
|
-
webhook_secret:
|
|
3204
|
+
company_id: z17.number().int().positive(),
|
|
3205
|
+
webhook_posts_url: z17.string().describe("Destination URL for outbound events. Empty string clears."),
|
|
3206
|
+
webhook_secret: z17.string().optional().describe("Shared secret used to sign payloads. Optional. Empty string clears.")
|
|
2258
3207
|
}
|
|
2259
3208
|
}, async ({ company_id, webhook_posts_url, webhook_secret }) => {
|
|
2260
3209
|
const updated = await client2.updateCompany(company_id, {
|
|
@@ -2278,8 +3227,8 @@ function registerWorkspaceSettingsTools(server2, client2, _options) {
|
|
|
2278
3227
|
title: "Set the workspace's left-menu visibility flags",
|
|
2279
3228
|
description: "Update which menu sections are visible in the Followr SPA for a workspace. The field `menu_visibility` is a map of section name to boolean. REPLACE semantics: the map passed becomes the new value. Useful for whitelabel installs that want to hide irrelevant sections.",
|
|
2280
3229
|
inputSchema: {
|
|
2281
|
-
company_id:
|
|
2282
|
-
menu_visibility:
|
|
3230
|
+
company_id: z17.number().int().positive(),
|
|
3231
|
+
menu_visibility: z17.record(z17.boolean()).describe("Full map of section_name -> visible. REPLACE, not merge.")
|
|
2283
3232
|
}
|
|
2284
3233
|
}, async ({ company_id, menu_visibility }) => {
|
|
2285
3234
|
const updated = await client2.updateCompany(company_id, {
|
|
@@ -2535,17 +3484,17 @@ function registerFollowrResources(server2, client2, _options) {
|
|
|
2535
3484
|
}
|
|
2536
3485
|
|
|
2537
3486
|
// ../mcp-core/dist/prompts/index.js
|
|
2538
|
-
import { z as
|
|
3487
|
+
import { z as z18 } from "zod";
|
|
2539
3488
|
function registerFollowrPrompts(server2, _client, _options) {
|
|
2540
3489
|
server2.registerPrompt("followr.weekly-brief", {
|
|
2541
3490
|
title: "Generate a week of scheduled posts from a brief",
|
|
2542
3491
|
description: "Take a free-form weekly brief and produce a full week of scheduled posts in the workspace. Anchors to the workspace's brand voice, picks suitable networks, drafts copy + images, and schedules across the week.",
|
|
2543
3492
|
argsSchema: {
|
|
2544
|
-
company_id:
|
|
2545
|
-
brief:
|
|
2546
|
-
networks:
|
|
2547
|
-
posts_per_day:
|
|
2548
|
-
starting_iso_date:
|
|
3493
|
+
company_id: z18.string().describe("Followr company id."),
|
|
3494
|
+
brief: z18.string().describe("Free-form brief for the week. Topics, hooks, must-mentions, banned terms, target audience notes."),
|
|
3495
|
+
networks: z18.string().optional().describe("Comma-separated network types (instagram, facebook, etc). Defaults to all connected accounts."),
|
|
3496
|
+
posts_per_day: z18.string().optional().describe("Integer; default 1."),
|
|
3497
|
+
starting_iso_date: z18.string().optional().describe("ISO 8601 date for day 1 of the week. Defaults to tomorrow at 10:00 in the workspace timezone.")
|
|
2549
3498
|
}
|
|
2550
3499
|
}, ({ company_id, brief, networks, posts_per_day, starting_iso_date }) => ({
|
|
2551
3500
|
messages: [
|
|
@@ -2578,14 +3527,14 @@ Procedure:
|
|
|
2578
3527
|
title: "Launch a multi-network campaign end-to-end",
|
|
2579
3528
|
description: "Spin up a full campaign in one shot: hashtag/tag taxonomy, brand-aligned hero asset, teaser + launch + follow-up posts across selected networks, scheduled around the launch date.",
|
|
2580
3529
|
argsSchema: {
|
|
2581
|
-
company_id:
|
|
2582
|
-
campaign_name:
|
|
2583
|
-
launch_iso_date:
|
|
2584
|
-
networks:
|
|
2585
|
-
product_or_offer:
|
|
2586
|
-
primary_cta:
|
|
2587
|
-
teaser_days_before:
|
|
2588
|
-
followup_days_after:
|
|
3530
|
+
company_id: z18.string().describe("Followr company id."),
|
|
3531
|
+
campaign_name: z18.string(),
|
|
3532
|
+
launch_iso_date: z18.string().describe("ISO 8601 launch datetime in UTC."),
|
|
3533
|
+
networks: z18.string().describe("Comma-separated network types."),
|
|
3534
|
+
product_or_offer: z18.string().describe("What the campaign is selling or announcing."),
|
|
3535
|
+
primary_cta: z18.string().describe("The single CTA (e.g. 'Sign up at acme.com/launch')."),
|
|
3536
|
+
teaser_days_before: z18.string().optional().describe("How many days before launch to start teasers. Default 3."),
|
|
3537
|
+
followup_days_after: z18.string().optional().describe("How many days after launch to keep follow-up posts. Default 7.")
|
|
2589
3538
|
}
|
|
2590
3539
|
}, ({ company_id, campaign_name, launch_iso_date, networks, product_or_offer, primary_cta, teaser_days_before, followup_days_after }) => ({
|
|
2591
3540
|
messages: [
|
|
@@ -2619,13 +3568,13 @@ Procedure:
|
|
|
2619
3568
|
title: "Generate an avatar video series on a single topic",
|
|
2620
3569
|
description: "Produce N short avatar videos on a topic. One script per episode, one lipsync render per episode, all scheduled across N consecutive days.",
|
|
2621
3570
|
argsSchema: {
|
|
2622
|
-
company_id:
|
|
2623
|
-
avatar_id:
|
|
2624
|
-
topic:
|
|
2625
|
-
episode_count:
|
|
2626
|
-
networks:
|
|
2627
|
-
starting_iso_date:
|
|
2628
|
-
cadence:
|
|
3571
|
+
company_id: z18.string(),
|
|
3572
|
+
avatar_id: z18.string().describe("Avatar to use (from list_avatars or create_avatar_full_flow)."),
|
|
3573
|
+
topic: z18.string().describe("The topic / theme of the series."),
|
|
3574
|
+
episode_count: z18.string().describe("Number of episodes (1-30 reasonable)."),
|
|
3575
|
+
networks: z18.string().describe("Comma-separated network types. Vertical 9:16 will be used for Reels / Shorts."),
|
|
3576
|
+
starting_iso_date: z18.string().describe("ISO 8601 datetime for episode 1."),
|
|
3577
|
+
cadence: z18.string().optional().describe("daily | every-other-day | weekly. Default daily.")
|
|
2629
3578
|
}
|
|
2630
3579
|
}, ({ company_id, avatar_id, topic, episode_count, networks, starting_iso_date, cadence }) => ({
|
|
2631
3580
|
messages: [
|
|
@@ -2656,10 +3605,10 @@ Procedure:
|
|
|
2656
3605
|
title: "Draft three crisis-response variants for review",
|
|
2657
3606
|
description: "Quickly produce three differently-toned crisis-response post drafts (apology, clarification, deflection) staged as drafts (not auto-publish) so a human can approve one.",
|
|
2658
3607
|
argsSchema: {
|
|
2659
|
-
company_id:
|
|
2660
|
-
situation:
|
|
2661
|
-
networks:
|
|
2662
|
-
urgency_window_hours:
|
|
3608
|
+
company_id: z18.string(),
|
|
3609
|
+
situation: z18.string().describe("What happened, key facts, sensitivity notes."),
|
|
3610
|
+
networks: z18.string().describe("Comma-separated network types."),
|
|
3611
|
+
urgency_window_hours: z18.string().optional().describe("How many hours until the post should ideally go live. Default 4.")
|
|
2663
3612
|
}
|
|
2664
3613
|
}, ({ company_id, situation, networks, urgency_window_hours }) => ({
|
|
2665
3614
|
messages: [
|
|
@@ -2693,11 +3642,11 @@ Procedure:
|
|
|
2693
3642
|
title: "Repurpose a single URL into multi-network posts",
|
|
2694
3643
|
description: "Given a URL (blog post, news article, video), produce network-tailored versions across the requested networks, with the right format (carousel, single image, video, blog post) per network.",
|
|
2695
3644
|
argsSchema: {
|
|
2696
|
-
company_id:
|
|
2697
|
-
source_url:
|
|
2698
|
-
networks:
|
|
2699
|
-
publish_at:
|
|
2700
|
-
include_visual:
|
|
3645
|
+
company_id: z18.string(),
|
|
3646
|
+
source_url: z18.string().describe("The URL to repurpose (article, blog post, video)."),
|
|
3647
|
+
networks: z18.string().describe("Comma-separated networks."),
|
|
3648
|
+
publish_at: z18.string().optional().describe("ISO 8601 datetime. If omitted, leave as draft."),
|
|
3649
|
+
include_visual: z18.string().optional().describe("If 'true', generate a fresh image per network format. Default 'true'.")
|
|
2701
3650
|
}
|
|
2702
3651
|
}, ({ company_id, source_url, networks, publish_at, include_visual }) => ({
|
|
2703
3652
|
messages: [
|
|
@@ -2735,6 +3684,8 @@ Procedure:
|
|
|
2735
3684
|
function registerFollowrTools(server2, client2, options = {}) {
|
|
2736
3685
|
registerCompanyTools(server2, client2, options);
|
|
2737
3686
|
registerPostGroupTools(server2, client2, options);
|
|
3687
|
+
registerPostTools(server2, client2, options);
|
|
3688
|
+
registerValidateTools(server2, client2, options);
|
|
2738
3689
|
registerTagTools(server2, client2, options);
|
|
2739
3690
|
registerAiResultsTools(server2, client2, options);
|
|
2740
3691
|
registerAvatarTools(server2, client2, options);
|
|
@@ -2782,7 +3733,7 @@ var client = new FollowrClient({
|
|
|
2782
3733
|
});
|
|
2783
3734
|
var server = new McpServer({
|
|
2784
3735
|
name: "followr",
|
|
2785
|
-
version: "0.
|
|
3736
|
+
version: "0.2.0"
|
|
2786
3737
|
});
|
|
2787
3738
|
registerFollowrTools(server, client);
|
|
2788
3739
|
var transport = new StdioServerTransport();
|