yandex-direct-mcp-plus 1.3.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.
Files changed (177) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +186 -0
  3. package/dist/app/index.d.ts +2 -0
  4. package/dist/app/index.js +15 -0
  5. package/dist/app/registry.d.ts +2 -0
  6. package/dist/app/registry.js +83 -0
  7. package/dist/app/server.d.ts +4 -0
  8. package/dist/app/server.js +30 -0
  9. package/dist/shared/api/client.d.ts +1 -0
  10. package/dist/shared/api/client.js +16 -0
  11. package/dist/shared/api/errors.d.ts +2 -0
  12. package/dist/shared/api/errors.js +21 -0
  13. package/dist/shared/api/fetch.d.ts +5 -0
  14. package/dist/shared/api/fetch.js +62 -0
  15. package/dist/shared/api/json.d.ts +2 -0
  16. package/dist/shared/api/json.js +13 -0
  17. package/dist/shared/api/parse.d.ts +2 -0
  18. package/dist/shared/api/parse.js +10 -0
  19. package/dist/shared/api/reports.d.ts +6 -0
  20. package/dist/shared/api/reports.js +45 -0
  21. package/dist/shared/api/v4.d.ts +1 -0
  22. package/dist/shared/api/v4.js +18 -0
  23. package/dist/shared/config/endpoints.d.ts +3 -0
  24. package/dist/shared/config/endpoints.js +8 -0
  25. package/dist/shared/config/enums.d.ts +30 -0
  26. package/dist/shared/config/enums.js +137 -0
  27. package/dist/shared/config/env.d.ts +2 -0
  28. package/dist/shared/config/env.js +14 -0
  29. package/dist/shared/config/limits.d.ts +58 -0
  30. package/dist/shared/config/limits.js +54 -0
  31. package/dist/shared/lib/campaign-type.d.ts +12 -0
  32. package/dist/shared/lib/campaign-type.js +20 -0
  33. package/dist/shared/lib/date.d.ts +3 -0
  34. package/dist/shared/lib/date.js +17 -0
  35. package/dist/shared/lib/format.d.ts +3 -0
  36. package/dist/shared/lib/format.js +51 -0
  37. package/dist/shared/lib/id.d.ts +4 -0
  38. package/dist/shared/lib/id.js +21 -0
  39. package/dist/shared/lib/money.d.ts +6 -0
  40. package/dist/shared/lib/money.js +21 -0
  41. package/dist/shared/lib/pagination.d.ts +9 -0
  42. package/dist/shared/lib/pagination.js +26 -0
  43. package/dist/shared/lib/report.d.ts +1 -0
  44. package/dist/shared/lib/report.js +5 -0
  45. package/dist/shared/lib/tool.d.ts +24 -0
  46. package/dist/shared/lib/tool.js +20 -0
  47. package/dist/tools/account/handler.d.ts +3 -0
  48. package/dist/tools/account/handler.js +12 -0
  49. package/dist/tools/account/schema.d.ts +4 -0
  50. package/dist/tools/account/schema.js +7 -0
  51. package/dist/tools/account/tool.d.ts +1 -0
  52. package/dist/tools/account/tool.js +11 -0
  53. package/dist/tools/ad-extensions/handler.d.ts +5 -0
  54. package/dist/tools/ad-extensions/handler.js +35 -0
  55. package/dist/tools/ad-extensions/schema.d.ts +14 -0
  56. package/dist/tools/ad-extensions/schema.js +37 -0
  57. package/dist/tools/ad-extensions/tool.d.ts +3 -0
  58. package/dist/tools/ad-extensions/tool.js +27 -0
  59. package/dist/tools/ad-groups/handler.d.ts +5 -0
  60. package/dist/tools/ad-groups/handler.js +33 -0
  61. package/dist/tools/ad-groups/schema.d.ts +14 -0
  62. package/dist/tools/ad-groups/schema.js +27 -0
  63. package/dist/tools/ad-groups/tool.d.ts +3 -0
  64. package/dist/tools/ad-groups/tool.js +27 -0
  65. package/dist/tools/ad-images/handler.d.ts +5 -0
  66. package/dist/tools/ad-images/handler.js +47 -0
  67. package/dist/tools/ad-images/schema.d.ts +13 -0
  68. package/dist/tools/ad-images/schema.js +37 -0
  69. package/dist/tools/ad-images/tool.d.ts +1 -0
  70. package/dist/tools/ad-images/tool.js +11 -0
  71. package/dist/tools/ads/handler.d.ts +7 -0
  72. package/dist/tools/ads/handler.js +54 -0
  73. package/dist/tools/ads/schema.d.ts +27 -0
  74. package/dist/tools/ads/schema.js +68 -0
  75. package/dist/tools/ads/tool.d.ts +5 -0
  76. package/dist/tools/ads/tool.js +43 -0
  77. package/dist/tools/audience-targets/handler.d.ts +6 -0
  78. package/dist/tools/audience-targets/handler.js +96 -0
  79. package/dist/tools/audience-targets/schema.d.ts +29 -0
  80. package/dist/tools/audience-targets/schema.js +84 -0
  81. package/dist/tools/audience-targets/tool.d.ts +2 -0
  82. package/dist/tools/audience-targets/tool.js +19 -0
  83. package/dist/tools/bid-adjustments/handler.d.ts +4 -0
  84. package/dist/tools/bid-adjustments/handler.js +43 -0
  85. package/dist/tools/bid-adjustments/schema.d.ts +16 -0
  86. package/dist/tools/bid-adjustments/schema.js +50 -0
  87. package/dist/tools/bid-adjustments/tool.d.ts +2 -0
  88. package/dist/tools/bid-adjustments/tool.js +19 -0
  89. package/dist/tools/businesses/handler.d.ts +3 -0
  90. package/dist/tools/businesses/handler.js +26 -0
  91. package/dist/tools/businesses/schema.d.ts +6 -0
  92. package/dist/tools/businesses/schema.js +12 -0
  93. package/dist/tools/businesses/tool.d.ts +1 -0
  94. package/dist/tools/businesses/tool.js +11 -0
  95. package/dist/tools/campaigns/handler.d.ts +11 -0
  96. package/dist/tools/campaigns/handler.js +200 -0
  97. package/dist/tools/campaigns/schema.d.ts +46 -0
  98. package/dist/tools/campaigns/schema.js +109 -0
  99. package/dist/tools/campaigns/tool.d.ts +7 -0
  100. package/dist/tools/campaigns/tool.js +61 -0
  101. package/dist/tools/changes/handler.d.ts +5 -0
  102. package/dist/tools/changes/handler.js +33 -0
  103. package/dist/tools/changes/schema.d.ts +9 -0
  104. package/dist/tools/changes/schema.js +35 -0
  105. package/dist/tools/changes/tool.d.ts +1 -0
  106. package/dist/tools/changes/tool.js +11 -0
  107. package/dist/tools/dictionaries/handler.d.ts +5 -0
  108. package/dist/tools/dictionaries/handler.js +66 -0
  109. package/dist/tools/dictionaries/schema.d.ts +9 -0
  110. package/dist/tools/dictionaries/schema.js +23 -0
  111. package/dist/tools/dictionaries/tool.d.ts +2 -0
  112. package/dist/tools/dictionaries/tool.js +19 -0
  113. package/dist/tools/dynamic-targets/handler.d.ts +6 -0
  114. package/dist/tools/dynamic-targets/handler.js +96 -0
  115. package/dist/tools/dynamic-targets/schema.d.ts +33 -0
  116. package/dist/tools/dynamic-targets/schema.js +91 -0
  117. package/dist/tools/dynamic-targets/tool.d.ts +2 -0
  118. package/dist/tools/dynamic-targets/tool.js +19 -0
  119. package/dist/tools/feeds/handler.d.ts +3 -0
  120. package/dist/tools/feeds/handler.js +29 -0
  121. package/dist/tools/feeds/schema.d.ts +6 -0
  122. package/dist/tools/feeds/schema.js +12 -0
  123. package/dist/tools/feeds/tool.d.ts +1 -0
  124. package/dist/tools/feeds/tool.js +11 -0
  125. package/dist/tools/keywords/handler.d.ts +6 -0
  126. package/dist/tools/keywords/handler.js +53 -0
  127. package/dist/tools/keywords/schema.d.ts +21 -0
  128. package/dist/tools/keywords/schema.js +41 -0
  129. package/dist/tools/keywords/tool.d.ts +4 -0
  130. package/dist/tools/keywords/tool.js +35 -0
  131. package/dist/tools/negative-keywords/handler.d.ts +8 -0
  132. package/dist/tools/negative-keywords/handler.js +135 -0
  133. package/dist/tools/negative-keywords/merge.d.ts +2 -0
  134. package/dist/tools/negative-keywords/merge.js +36 -0
  135. package/dist/tools/negative-keywords/schema.d.ts +37 -0
  136. package/dist/tools/negative-keywords/schema.js +139 -0
  137. package/dist/tools/negative-keywords/shared-set-link.d.ts +3 -0
  138. package/dist/tools/negative-keywords/shared-set-link.js +21 -0
  139. package/dist/tools/negative-keywords/tool.d.ts +6 -0
  140. package/dist/tools/negative-keywords/tool.js +51 -0
  141. package/dist/tools/retargeting/handler.d.ts +4 -0
  142. package/dist/tools/retargeting/handler.js +47 -0
  143. package/dist/tools/retargeting/schema.d.ts +19 -0
  144. package/dist/tools/retargeting/schema.js +57 -0
  145. package/dist/tools/retargeting/tool.d.ts +2 -0
  146. package/dist/tools/retargeting/tool.js +19 -0
  147. package/dist/tools/search-queries/handler.d.ts +3 -0
  148. package/dist/tools/search-queries/handler.js +28 -0
  149. package/dist/tools/search-queries/schema.d.ts +7 -0
  150. package/dist/tools/search-queries/schema.js +17 -0
  151. package/dist/tools/search-queries/tool.d.ts +1 -0
  152. package/dist/tools/search-queries/tool.js +11 -0
  153. package/dist/tools/sitelinks/handler.d.ts +4 -0
  154. package/dist/tools/sitelinks/handler.js +28 -0
  155. package/dist/tools/sitelinks/schema.d.ts +13 -0
  156. package/dist/tools/sitelinks/schema.js +35 -0
  157. package/dist/tools/sitelinks/tool.d.ts +2 -0
  158. package/dist/tools/sitelinks/tool.js +19 -0
  159. package/dist/tools/statistics/handler.d.ts +3 -0
  160. package/dist/tools/statistics/handler.js +20 -0
  161. package/dist/tools/statistics/schema.d.ts +7 -0
  162. package/dist/tools/statistics/schema.js +17 -0
  163. package/dist/tools/statistics/tool.d.ts +1 -0
  164. package/dist/tools/statistics/tool.js +11 -0
  165. package/dist/tools/time-targeting/handler.d.ts +6 -0
  166. package/dist/tools/time-targeting/handler.js +124 -0
  167. package/dist/tools/time-targeting/schema.d.ts +19 -0
  168. package/dist/tools/time-targeting/schema.js +73 -0
  169. package/dist/tools/time-targeting/tool.d.ts +2 -0
  170. package/dist/tools/time-targeting/tool.js +20 -0
  171. package/dist/tools/vcards/handler.d.ts +4 -0
  172. package/dist/tools/vcards/handler.js +105 -0
  173. package/dist/tools/vcards/schema.d.ts +27 -0
  174. package/dist/tools/vcards/schema.js +43 -0
  175. package/dist/tools/vcards/tool.d.ts +2 -0
  176. package/dist/tools/vcards/tool.js +19 -0
  177. package/package.json +121 -0
@@ -0,0 +1,11 @@
1
+ import { defineTool, READ } from "#shared/lib/tool";
2
+ import { handleGetStatistics } from "./handler.js";
3
+ import { getStatisticsSchema } from "./schema.js";
4
+ export const getStatisticsTool = defineTool({
5
+ name: "get_statistics",
6
+ title: "Статистика",
7
+ description: "Статистика кампаний за период: показы, клики, расход (руб), CTR, CPC (ReportService, TSV).",
8
+ annotations: READ,
9
+ schema: getStatisticsSchema,
10
+ handler: handleGetStatistics
11
+ });
@@ -0,0 +1,6 @@
1
+ import type { z } from "zod";
2
+ import type { getTimeTargetingSchema, setTimeTargetingSchema } from "./schema.js";
3
+ type SetParams = z.infer<typeof setTimeTargetingSchema>;
4
+ export declare function handleSetTimeTargeting(params: SetParams): Promise<string>;
5
+ export declare function handleGetTimeTargeting(params: z.infer<typeof getTimeTargetingSchema>): Promise<string>;
6
+ export {};
@@ -0,0 +1,124 @@
1
+ import { apiPost } from "#shared/api/client";
2
+ import { WEEKDAYS } from "#shared/config/enums";
3
+ import { HOURS_IN_DAY } from "#shared/config/limits";
4
+ import { formatResult } from "#shared/lib/format";
5
+ import { apiId } from "#shared/lib/id";
6
+ const FIELDS = ["Id", "Name", "TimeZone", "TimeTargeting"];
7
+ // День недели в строке Schedule — число: 1 — понедельник, 7 — воскресенье.
8
+ const DAY_NUMBERS = new Map(WEEKDAYS.map((day, index) => [day, index + 1]));
9
+ // Расписание собирается сразу на все семь дней. Пропустить день нельзя: Директ
10
+ // трактует отсутствующий элемент как «круглосуточно 100%», и «показы только в
11
+ // будни» молча превратилось бы в показы всю неделю.
12
+ function compileSchedule(rules) {
13
+ const week = WEEKDAYS.map(() => new Array(HOURS_IN_DAY).fill(0));
14
+ for (const rule of rules) {
15
+ for (const day of rule.days) {
16
+ const hours = week[DAY_NUMBERS.get(day) - 1];
17
+ for (let hour = rule.start_hour; hour < rule.end_hour; hour++)
18
+ hours[hour] = rule.bid_percent;
19
+ }
20
+ }
21
+ return week.map((hours, index) => [index + 1, ...hours].join(","));
22
+ }
23
+ function buildHolidaysSchedule(params) {
24
+ const configured = params.suspend_on_holidays !== undefined ||
25
+ params.holiday_bid_percent !== undefined ||
26
+ params.holiday_start_hour !== undefined ||
27
+ params.holiday_end_hour !== undefined;
28
+ if (!configured)
29
+ return undefined;
30
+ const schedule = {
31
+ SuspendOnHolidays: params.suspend_on_holidays ? "YES" : "NO"
32
+ };
33
+ if (params.holiday_bid_percent !== undefined)
34
+ schedule.BidPercent = params.holiday_bid_percent;
35
+ if (params.holiday_start_hour !== undefined)
36
+ schedule.StartHour = params.holiday_start_hour;
37
+ if (params.holiday_end_hour !== undefined)
38
+ schedule.EndHour = params.holiday_end_hour;
39
+ return schedule;
40
+ }
41
+ export async function handleSetTimeTargeting(params) {
42
+ const timeTargeting = {
43
+ Schedule: { Items: compileSchedule(params.schedule) },
44
+ ConsiderWorkingWeekends: params.consider_working_weekends ? "YES" : "NO"
45
+ };
46
+ const holidays = buildHolidaysSchedule(params);
47
+ if (holidays)
48
+ timeTargeting.HolidaysSchedule = holidays;
49
+ const campaign = { Id: apiId(params.campaign_id), TimeTargeting: timeTargeting };
50
+ if (params.time_zone !== undefined)
51
+ campaign.TimeZone = params.time_zone;
52
+ return formatResult(await apiPost("campaigns", "update", { Campaigns: [campaign] }));
53
+ }
54
+ // Часы с одинаковым коэффициентом сворачиваются в интервал: 24 числа в строке
55
+ // модель прочитает, но пересказать пользователю «когда идут показы» по ним сложно.
56
+ function toIntervals(hours) {
57
+ const intervals = [];
58
+ for (let hour = 0; hour < HOURS_IN_DAY; hour++) {
59
+ const percent = hours[hour];
60
+ if (percent === 0)
61
+ continue;
62
+ const last = intervals[intervals.length - 1];
63
+ if (last && last.To === hour && last.BidPercent === percent)
64
+ last.To = hour + 1;
65
+ else
66
+ intervals.push({ From: hour, To: hour + 1, BidPercent: percent });
67
+ }
68
+ return intervals;
69
+ }
70
+ function describe(intervals) {
71
+ if (intervals.length === 0)
72
+ return "показов нет";
73
+ if (intervals.length === 1 && intervals[0].From === 0 && intervals[0].To === HOURS_IN_DAY) {
74
+ return intervals[0].BidPercent === 100 ? "круглосуточно" : `круглосуточно, ${intervals[0].BidPercent}%`;
75
+ }
76
+ return intervals
77
+ .map(({ From, To, BidPercent }) => `${From}–${To}${BidPercent === 100 ? "" : ` (${BidPercent}%)`}`)
78
+ .join(", ");
79
+ }
80
+ // Отсутствующий в ответе день — круглосуточные показы со 100%: таково правило
81
+ // самого Директа, а не наша догадка. Поэтому неделя заполняется сотнями, и уже
82
+ // поверх ложатся присланные строки.
83
+ function decodeSchedule(items) {
84
+ const week = WEEKDAYS.map(() => new Array(HOURS_IN_DAY).fill(100));
85
+ for (const item of Array.isArray(items) ? items : []) {
86
+ const numbers = String(item)
87
+ .split(",")
88
+ .map((part) => Number(part.trim()));
89
+ const dayNumber = numbers[0];
90
+ if (!Number.isInteger(dayNumber) || dayNumber < 1 || dayNumber > WEEKDAYS.length)
91
+ continue;
92
+ week[dayNumber - 1] = numbers.slice(1, HOURS_IN_DAY + 1).map((percent) => (Number.isFinite(percent) ? percent : 0));
93
+ }
94
+ return week.map((hours, index) => {
95
+ const intervals = toIntervals(hours);
96
+ return { Day: WEEKDAYS[index], Around: describe(intervals), Intervals: intervals };
97
+ });
98
+ }
99
+ function scheduleItems(targeting) {
100
+ const schedule = targeting?.Schedule;
101
+ if (Array.isArray(schedule))
102
+ return schedule;
103
+ return schedule?.Items;
104
+ }
105
+ export async function handleGetTimeTargeting(params) {
106
+ const data = (await apiPost("campaigns", "get", {
107
+ SelectionCriteria: { Ids: [apiId(params.campaign_id)] },
108
+ FieldNames: FIELDS
109
+ }));
110
+ const campaign = data?.result?.Campaigns?.[0];
111
+ if (!campaign)
112
+ return formatResult(data, { money: false });
113
+ const items = scheduleItems(campaign.TimeTargeting);
114
+ const note = items === undefined ? "ℹ️ Расписание не задано: показы идут круглосуточно.\n\n" : "";
115
+ return (note +
116
+ formatResult({
117
+ CampaignId: campaign.Id,
118
+ Name: campaign.Name,
119
+ TimeZone: campaign.TimeZone ?? "Europe/Moscow",
120
+ ConsiderWorkingWeekends: campaign.TimeTargeting?.ConsiderWorkingWeekends === "YES",
121
+ HolidaysSchedule: campaign.TimeTargeting?.HolidaysSchedule ?? null,
122
+ Schedule: decodeSchedule(items)
123
+ }, { money: false }));
124
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export declare const getTimeTargetingSchema: z.ZodObject<{
3
+ campaign_id: z.ZodString;
4
+ }, z.core.$strip>;
5
+ export declare const setTimeTargetingSchema: z.ZodObject<{
6
+ campaign_id: z.ZodString;
7
+ schedule: z.ZodArray<z.ZodObject<{
8
+ days: z.ZodArray<z.ZodLiteral<"FRI" | "MON" | "SAT" | "SUN" | "THU" | "TUE" | "WED">>;
9
+ start_hour: z.ZodInt;
10
+ end_hour: z.ZodInt;
11
+ bid_percent: z.ZodDefault<z.ZodInt>;
12
+ }, z.core.$strip>>;
13
+ consider_working_weekends: z.ZodDefault<z.ZodBoolean>;
14
+ suspend_on_holidays: z.ZodOptional<z.ZodBoolean>;
15
+ holiday_bid_percent: z.ZodOptional<z.ZodInt>;
16
+ holiday_start_hour: z.ZodOptional<z.ZodInt>;
17
+ holiday_end_hour: z.ZodOptional<z.ZodInt>;
18
+ time_zone: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strip>;
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { WEEKDAYS } from "#shared/config/enums";
3
+ import { HOLIDAY_BID_RANGE, HOURLY_BID_RANGE } from "#shared/config/limits";
4
+ import { idField } from "#shared/lib/id";
5
+ export const getTimeTargetingSchema = z.object({
6
+ campaign_id: idField("ID кампании, десятичная строка")
7
+ });
8
+ // Час — это z.int(), а не z.number().int(): в zod v4 целое есть отдельным типом,
9
+ // и границы задаются проверками, а не цепочкой методов v3.
10
+ const startHour = (description) => z
11
+ .int()
12
+ .check(z.gte(0, { error: "Час начала не меньше 0" }), z.lte(23, { error: "Час начала не больше 23" }))
13
+ .meta({ description });
14
+ const endHour = (description) => z
15
+ .int()
16
+ .check(z.gte(1, { error: "Час окончания не меньше 1" }), z.lte(24, { error: "Час окончания не больше 24" }))
17
+ .meta({ description });
18
+ const bidPercent = (range, description) => z
19
+ .int()
20
+ .check(z.gte(range.min, { error: `Коэффициент не меньше ${range.min}%` }), z.lte(range.max, { error: `Коэффициент не больше ${range.max}%` }), z.multipleOf(range.step, { error: `Коэффициент кратен ${range.step}` }))
21
+ .meta({ description });
22
+ // Наружу расписание задаётся правилами «дни + интервал часов + коэффициент», а не
23
+ // строками из 25 чисел, как в API: собрать такую строку модель обязана без ошибок,
24
+ // а проверить её глазами нельзя. Компиляцию правил в строки делает хендлер.
25
+ const scheduleRule = z
26
+ .object({
27
+ days: z
28
+ .array(z.literal(WEEKDAYS))
29
+ .check(z.minLength(1, { error: "Укажите хотя бы один день недели" }))
30
+ .meta({ description: "Дни недели, на которые распространяется правило: MON, TUE, WED, THU, FRI, SAT, SUN" }),
31
+ start_hour: startHour("Час начала показов включительно, 0–23"),
32
+ end_hour: endHour("Час окончания показов, не включая его: end_hour=21 — показы идут до 20:59. 1–24"),
33
+ bid_percent: bidPercent(HOURLY_BID_RANGE, `Коэффициент к ставке в эти часы, % от текущей: ${HOURLY_BID_RANGE.min}–${HOURLY_BID_RANGE.max} с шагом ${HOURLY_BID_RANGE.step}, 100 — без изменений`).default(100)
34
+ })
35
+ .refine((rule) => rule.end_hour > rule.start_hour, {
36
+ error: "end_hour должен быть больше start_hour: интервал 9–21 — это start_hour=9, end_hour=21"
37
+ });
38
+ export const setTimeTargetingSchema = z
39
+ .object({
40
+ campaign_id: idField("ID кампании, десятичная строка"),
41
+ schedule: z
42
+ .array(scheduleRule)
43
+ .check(z.minLength(1, { error: "Расписание пустое: без единого правила показов не будет вовсе" }))
44
+ .meta({
45
+ description: "Правила показов. Часы, не покрытые ни одним правилом, показов не получают — расписание задаётся целиком, а не дополняется. Правила применяются по порядку, последнее переопределяет предыдущие"
46
+ }),
47
+ consider_working_weekends: z.boolean().default(true).meta({
48
+ description: "Показывать ли в рабочие выходные по расписанию переносимого буднего дня; false — по расписанию выходного"
49
+ }),
50
+ suspend_on_holidays: z.boolean().optional().meta({
51
+ description: "true — в праздники показов нет; false — идут по правилам holiday_*. Не задан — праздники отдельно не настраиваются"
52
+ }),
53
+ holiday_bid_percent: bidPercent(HOLIDAY_BID_RANGE, `Коэффициент к ставке в праздники, % от текущей: ${HOLIDAY_BID_RANGE.min}–${HOLIDAY_BID_RANGE.max} с шагом ${HOLIDAY_BID_RANGE.step}. Ноль запрещён — показы отключает suspend_on_holidays`).optional(),
54
+ holiday_start_hour: startHour("Час начала показов в праздники включительно, 0–23").optional(),
55
+ holiday_end_hour: endHour("Час окончания показов в праздники, не включая его, 1–24").optional(),
56
+ time_zone: z
57
+ .string()
58
+ .check(z.minLength(1, { error: "Часовой пояс не может быть пустым" }))
59
+ .optional()
60
+ .meta({
61
+ description: "Часовой пояс кампании, например Europe/Moscow. Допустимые значения — в справочнике list_time_zones. Не задан — остаётся прежним"
62
+ })
63
+ })
64
+ // Директ отклоняет часы и коэффициент праздников при SuspendOnHolidays=YES.
65
+ // Ловим это схемой: иначе модель отправит взаимоисключающие настройки и получит
66
+ // ошибку API вместо подсказки.
67
+ .refine((params) => params.suspend_on_holidays !== true ||
68
+ (params.holiday_bid_percent === undefined &&
69
+ params.holiday_start_hour === undefined &&
70
+ params.holiday_end_hour === undefined), { error: "При suspend_on_holidays=true часы и коэффициент праздников задавать нельзя: показов не будет вовсе" })
71
+ .refine((params) => params.holiday_start_hour === undefined ||
72
+ params.holiday_end_hour === undefined ||
73
+ params.holiday_end_hour > params.holiday_start_hour, { error: "holiday_end_hour должен быть больше holiday_start_hour" });
@@ -0,0 +1,2 @@
1
+ export declare const getTimeTargetingTool: import("#shared/lib/tool").ToolDescriptor;
2
+ export declare const setTimeTargetingTool: import("#shared/lib/tool").ToolDescriptor;
@@ -0,0 +1,20 @@
1
+ import { defineTool, IDEMPOTENT, READ } from "#shared/lib/tool";
2
+ import { handleGetTimeTargeting, handleSetTimeTargeting } from "./handler.js";
3
+ import { getTimeTargetingSchema, setTimeTargetingSchema } from "./schema.js";
4
+ export const getTimeTargetingTool = defineTool({
5
+ name: "get_time_targeting",
6
+ title: "Расписание показов кампании",
7
+ description: "Временной таргетинг кампании: часовой пояс, часы показов по дням недели, почасовые коэффициенты и настройка праздников.",
8
+ annotations: READ,
9
+ schema: getTimeTargetingSchema,
10
+ handler: handleGetTimeTargeting
11
+ });
12
+ export const setTimeTargetingTool = defineTool({
13
+ name: "set_time_targeting",
14
+ title: "Задать расписание показов",
15
+ description: "Задать расписание показов кампании: дни, часы, почасовые коэффициенты, праздники и часовой пояс. " +
16
+ "⚠️ Расписание заменяется целиком: часы вне переданных правил показов не получат.",
17
+ annotations: IDEMPOTENT,
18
+ schema: setTimeTargetingSchema,
19
+ handler: handleSetTimeTargeting
20
+ });
@@ -0,0 +1,4 @@
1
+ import type { z } from "zod";
2
+ import type { addVcardSchema, listVcardsSchema } from "./schema.js";
3
+ export declare function handleListVcards(params: z.infer<typeof listVcardsSchema>): Promise<string>;
4
+ export declare function handleAddVcard(params: z.infer<typeof addVcardSchema>): Promise<string>;
@@ -0,0 +1,105 @@
1
+ import { apiPost } from "#shared/api/client";
2
+ import { PAGE_MAX_LIMIT } from "#shared/config/limits";
3
+ import { formatResult } from "#shared/lib/format";
4
+ import { apiId, apiIds } from "#shared/lib/id";
5
+ import { buildPage } from "#shared/lib/pagination";
6
+ const NO_MONEY = { money: false };
7
+ const DECIMAL_ID = /^[1-9]\d*$/;
8
+ const VCARD_FIELDS = [
9
+ "Id",
10
+ "CampaignId",
11
+ "Country",
12
+ "City",
13
+ "CompanyName",
14
+ "WorkTime",
15
+ "Phone",
16
+ "Street",
17
+ "House",
18
+ "Building",
19
+ "Apartment",
20
+ "InstantMessenger",
21
+ "ExtraMessage",
22
+ "ContactEmail",
23
+ "Ogrn",
24
+ "ContactPerson",
25
+ "MetroStationId"
26
+ ];
27
+ // VCardId приезжает строкой (json-bigint) либо числом, если ID короткий.
28
+ // Чистая функция, поэтому проверяется без сети.
29
+ function collectVCardIds(response) {
30
+ const ids = new Set();
31
+ for (const ad of response.result?.Ads ?? []) {
32
+ const value = ad.TextAd?.VCardId;
33
+ if (typeof value === "string" && DECIMAL_ID.test(value))
34
+ ids.add(value);
35
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
36
+ ids.add(String(value));
37
+ }
38
+ return [...ids];
39
+ }
40
+ async function findVCardIdsByCampaigns(campaignIds) {
41
+ const response = (await apiPost("ads", "get", {
42
+ SelectionCriteria: { CampaignIds: apiIds(campaignIds) },
43
+ FieldNames: ["Id"],
44
+ TextAdFieldNames: ["VCardId"],
45
+ Page: { Limit: PAGE_MAX_LIMIT }
46
+ }));
47
+ return collectVCardIds(response);
48
+ }
49
+ export async function handleListVcards(params) {
50
+ const ids = new Set(params.vcard_ids ?? []);
51
+ if (params.campaign_ids?.length) {
52
+ for (const id of await findVCardIdsByCampaigns(params.campaign_ids))
53
+ ids.add(id);
54
+ }
55
+ if (ids.size === 0) {
56
+ // Поиск по кампаниям, не нашедший визиток, — обычный пустой результат, а не ошибка.
57
+ if (params.campaign_ids?.length)
58
+ return formatResult({ result: { VCards: [] } }, NO_MONEY);
59
+ throw new Error("Передайте vcard_ids и/или campaign_ids.");
60
+ }
61
+ const request = {
62
+ SelectionCriteria: { Ids: apiIds([...ids]) },
63
+ FieldNames: VCARD_FIELDS
64
+ };
65
+ const page = buildPage(params);
66
+ if (page)
67
+ request.Page = page;
68
+ return formatResult(await apiPost("vcards", "get", request), NO_MONEY);
69
+ }
70
+ // Необязательные поля визитки: имя параметра → имя поля Директа.
71
+ const OPTIONAL_FIELDS = [
72
+ ["street", "Street"],
73
+ ["house", "House"],
74
+ ["building", "Building"],
75
+ ["apartment", "Apartment"],
76
+ ["extra_message", "ExtraMessage"],
77
+ ["contact_email", "ContactEmail"],
78
+ ["ogrn", "Ogrn"],
79
+ ["contact_person", "ContactPerson"]
80
+ ];
81
+ export async function handleAddVcard(params) {
82
+ const phone = {
83
+ CountryCode: params.phone_country_code,
84
+ CityCode: params.phone_city_code,
85
+ PhoneNumber: params.phone_number
86
+ };
87
+ if (params.phone_extension !== undefined)
88
+ phone.Extension = params.phone_extension;
89
+ const vcard = {
90
+ CampaignId: apiId(params.campaign_id),
91
+ Country: params.country,
92
+ City: params.city,
93
+ CompanyName: params.company_name,
94
+ WorkTime: params.work_time,
95
+ Phone: phone
96
+ };
97
+ for (const [source, target] of OPTIONAL_FIELDS) {
98
+ const value = params[source];
99
+ if (value !== undefined)
100
+ vcard[target] = value;
101
+ }
102
+ if (params.metro_station_id)
103
+ vcard.MetroStationId = apiId(params.metro_station_id);
104
+ return formatResult(await apiPost("vcards", "add", { VCards: [vcard] }), NO_MONEY);
105
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export declare const listVcardsSchema: z.ZodObject<{
3
+ limit: z.ZodOptional<z.ZodInt>;
4
+ offset: z.ZodOptional<z.ZodInt>;
5
+ vcard_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ campaign_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ }, z.core.$strip>;
8
+ export declare const addVcardSchema: z.ZodObject<{
9
+ campaign_id: z.ZodString;
10
+ country: z.ZodString;
11
+ city: z.ZodString;
12
+ company_name: z.ZodString;
13
+ work_time: z.ZodString;
14
+ phone_country_code: z.ZodString;
15
+ phone_city_code: z.ZodString;
16
+ phone_number: z.ZodString;
17
+ phone_extension: z.ZodOptional<z.ZodString>;
18
+ street: z.ZodOptional<z.ZodString>;
19
+ house: z.ZodOptional<z.ZodString>;
20
+ building: z.ZodOptional<z.ZodString>;
21
+ apartment: z.ZodOptional<z.ZodString>;
22
+ extra_message: z.ZodOptional<z.ZodString>;
23
+ contact_email: z.ZodOptional<z.ZodEmail>;
24
+ ogrn: z.ZodOptional<z.ZodString>;
25
+ contact_person: z.ZodOptional<z.ZodString>;
26
+ metro_station_id: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { MAX_CAMPAIGNS_PER_ADJUSTMENT_CALL, MAX_IDS_PER_CALL } from "#shared/config/limits";
3
+ import { idField } from "#shared/lib/id";
4
+ import { pageFields } from "#shared/lib/pagination";
5
+ // Обязательное поле визитки: непустая строка с собственным текстом ошибки.
6
+ const required = (error, description) => z.string().check(z.minLength(1, { error })).meta({ description });
7
+ export const listVcardsSchema = z.object({
8
+ vcard_ids: z
9
+ .array(idField("ID визитки"))
10
+ .check(z.maxLength(MAX_IDS_PER_CALL, { error: `За один вызов допустимо не больше ${MAX_IDS_PER_CALL} визиток` }))
11
+ .optional()
12
+ .meta({ description: "Конкретные визитки по их ID" }),
13
+ campaign_ids: z
14
+ .array(idField("ID кампании"))
15
+ .check(z.maxLength(MAX_CAMPAIGNS_PER_ADJUSTMENT_CALL, {
16
+ error: `За один вызов допустимо не больше ${MAX_CAMPAIGNS_PER_ADJUSTMENT_CALL} кампаний`
17
+ }))
18
+ .optional()
19
+ .meta({ description: "Найти визитки, привязанные к объявлениям этих кампаний" }),
20
+ ...pageFields
21
+ });
22
+ export const addVcardSchema = z.object({
23
+ campaign_id: idField("ID кампании, к которой привязывается визитка"),
24
+ country: required("Страна обязательна", "Страна, например «Россия»"),
25
+ city: required("Город обязателен", "Город"),
26
+ company_name: required("Название организации обязательно", "Название организации"),
27
+ work_time: required("Режим работы обязателен", "Режим работы в формате API: день_с#день_по#час_с#мин_с#час_по#мин_по, например 1#5#9#0#18#0"),
28
+ phone_country_code: required("Код страны обязателен", "Код страны, «+7»"),
29
+ phone_city_code: required("Код города обязателен", "Код города или оператора"),
30
+ phone_number: required("Номер телефона обязателен", "Номер телефона"),
31
+ phone_extension: z.string().optional().meta({ description: "Добавочный номер" }),
32
+ street: z.string().optional().meta({ description: "Улица" }),
33
+ house: z.string().optional().meta({ description: "Дом" }),
34
+ building: z.string().optional().meta({ description: "Корпус" }),
35
+ apartment: z.string().optional().meta({ description: "Офис или квартира" }),
36
+ extra_message: z.string().optional().meta({ description: "Дополнительная информация в визитке" }),
37
+ contact_email: z.email({ error: "Некорректный адрес электронной почты" }).optional().meta({
38
+ description: "Контактный адрес электронной почты"
39
+ }),
40
+ ogrn: z.string().optional().meta({ description: "ОГРН или ОГРНИП организации" }),
41
+ contact_person: z.string().optional().meta({ description: "Контактное лицо" }),
42
+ metro_station_id: idField("ID станции метро из справочника MetroStations").optional()
43
+ });
@@ -0,0 +1,2 @@
1
+ export declare const listVcardsTool: import("#shared/lib/tool").ToolDescriptor;
2
+ export declare const addVcardTool: import("#shared/lib/tool").ToolDescriptor;
@@ -0,0 +1,19 @@
1
+ import { defineTool, READ, WRITE } from "#shared/lib/tool";
2
+ import { handleAddVcard, handleListVcards } from "./handler.js";
3
+ import { addVcardSchema, listVcardsSchema } from "./schema.js";
4
+ export const listVcardsTool = defineTool({
5
+ name: "list_vcards",
6
+ title: "Список визиток",
7
+ description: "Получить виртуальные визитки по ID или найти их через объявления выбранных кампаний.",
8
+ annotations: READ,
9
+ schema: listVcardsSchema,
10
+ handler: handleListVcards
11
+ });
12
+ export const addVcardTool = defineTool({
13
+ name: "add_vcard",
14
+ title: "Создать визитку",
15
+ description: "Создать виртуальную визитку для кампании.",
16
+ annotations: WRITE,
17
+ schema: addVcardSchema,
18
+ handler: handleAddVcard
19
+ });
package/package.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "yandex-direct-mcp-plus",
3
+ "version": "1.3.0",
4
+ "description": "MCP-сервер для API Яндекс.Директа v5: кампании, объявления, ключевые фразы, ставки, минус-фразы, статистика. Деньги в рублях, ID без потери точности.",
5
+ "type": "module",
6
+ "main": "dist/app/index.js",
7
+ "imports": {
8
+ "#shared/*": {
9
+ "types": "./src/shared/*.ts",
10
+ "development": "./src/shared/*.ts",
11
+ "default": "./dist/shared/*.js"
12
+ },
13
+ "#tools/*": {
14
+ "types": "./src/tools/*.ts",
15
+ "development": "./src/tools/*.ts",
16
+ "default": "./dist/tools/*.js"
17
+ },
18
+ "#app/*": {
19
+ "types": "./src/app/*.ts",
20
+ "development": "./src/app/*.ts",
21
+ "default": "./dist/app/*.js"
22
+ },
23
+ "#testing/*": {
24
+ "types": "./src/testing/*.ts",
25
+ "development": "./src/testing/*.ts",
26
+ "default": "./dist/testing/*.js"
27
+ }
28
+ },
29
+ "bin": {
30
+ "yandex-direct-mcp-plus": "dist/app/index.js"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22.0.0"
37
+ },
38
+ "scripts": {
39
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
40
+ "build": "npm run clean && tsc -p tsconfig.build.json",
41
+ "dev": "tsx --env-file-if-exists=.env --conditions=development src/app/index.ts",
42
+ "probe": "node --env-file-if-exists=.env --import tsx --conditions=development",
43
+ "start": "node --env-file-if-exists=.env dist/app/index.js",
44
+ "test": "vitest run --project unit",
45
+ "test:watch": "vitest --project unit",
46
+ "test:int": "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --project int",
47
+ "prepublishOnly": "npm run build",
48
+ "typecheck": "tsc --noEmit",
49
+ "lint": "biome check",
50
+ "lint:fix": "biome check --write",
51
+ "lint:dead": "knip",
52
+ "api:snapshot": "node scripts/wsdl-snapshot.mjs",
53
+ "prepare": "lefthook install"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.30.0",
57
+ "json-bigint": "^1.0.0",
58
+ "zod": "^4.5.4"
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "^2.5.12",
62
+ "@commitlint/cli": "^21.2.2",
63
+ "@commitlint/config-conventional": "^21.2.2",
64
+ "@commitlint/types": "^21.2.0",
65
+ "@semantic-release/changelog": "^7.0.0",
66
+ "@semantic-release/git": "^11.0.1",
67
+ "@types/json-bigint": "^1.0.4",
68
+ "@types/node": "^26.4.1",
69
+ "@vitest/coverage-v8": "^4.1.11",
70
+ "knip": "^6.34.0",
71
+ "lefthook": "^2.1.12",
72
+ "semantic-release": "^25.0.9",
73
+ "tsx": "^4.23.13",
74
+ "typescript": "^7.0.2",
75
+ "vitest": "^4.1.11"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "keywords": [
81
+ "mcp",
82
+ "mcp-server",
83
+ "model-context-protocol",
84
+ "modelcontextprotocol",
85
+ "claude",
86
+ "ai",
87
+ "llm",
88
+ "anthropic",
89
+ "cursor",
90
+ "ai-tools",
91
+ "ai-agent",
92
+ "russian-api",
93
+ "yandex",
94
+ "direct",
95
+ "advertising",
96
+ "ppc",
97
+ "ads",
98
+ "marketing",
99
+ "typescript",
100
+ "yandex-direct",
101
+ "api"
102
+ ],
103
+ "license": "MIT",
104
+ "repository": {
105
+ "type": "git",
106
+ "url": "git+https://github.com/Pavelsiba/yandex-direct-mcp-plus.git"
107
+ },
108
+ "homepage": "https://github.com/Pavelsiba/yandex-direct-mcp-plus#readme",
109
+ "author": "Pavelsiba (https://github.com/Pavelsiba)",
110
+ "bugs": {
111
+ "url": "https://github.com/Pavelsiba/yandex-direct-mcp-plus/issues"
112
+ },
113
+ "contributors": [
114
+ "theYahia (https://github.com/theYahia)",
115
+ "Maxim DrSeedon (https://github.com/DrSeedon)"
116
+ ],
117
+ "allowScripts": {
118
+ "esbuild@0.28.2": true,
119
+ "lefthook@2.1.12": true
120
+ }
121
+ }