certifica-mcp 0.1.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/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ CERTIFICA_API_URL=https://api.exemplo.com
2
+ CERTIFICA_API_TOKEN=certifica_token_individual
3
+ CERTIFICA_API_KEY=api_key_da_aplicacao
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Certifica MCP
2
+
3
+ Servidor MCP para integrar Codex, Claude e outros clientes compatíveis ao fluxo
4
+ editorial do blog Certifica.
5
+
6
+ O projeto será uma camada fina sobre `certifica-backend`. Ele não acessará
7
+ PostgreSQL, Prisma, S3 ou Higgsfield diretamente. Suas ferramentas transformarão
8
+ chamadas MCP em requisições autenticadas à API.
9
+
10
+ ## Objetivos
11
+
12
+ - consultar posts e mídias;
13
+ - enviar imagens geradas ou locais;
14
+ - validar artigos;
15
+ - criar e editar rascunhos;
16
+ - agendar ou publicar somente com autorização explícita;
17
+ - funcionar localmente e exclusivamente por `stdio`;
18
+ - ser utilizado pela skill editorial `seo-blog-arquitetura`.
19
+
20
+ ## Princípios
21
+
22
+ - `DRAFT` é o estado padrão.
23
+ - Publicação é uma ferramenta e uma permissão separadas.
24
+ - A API é a autoridade de autenticação, validação e persistência.
25
+ - Credenciais nunca entram no código, na skill ou nos argumentos visíveis.
26
+ - Ferramentas retornam dados estruturados e erros acionáveis.
27
+ - Operações mutáveis usam idempotência e deixam trilha de auditoria.
28
+
29
+ ## Documentação
30
+
31
+ - [Arquitetura](docs/ARCHITECTURE.md)
32
+ - [Configuração no Codex e Claude](docs/CLIENTS.md)
33
+ - [Roadmap](ROADMAP.md)
34
+
35
+ ## Desenvolvimento
36
+
37
+ ```bash
38
+ npm install
39
+ npm test
40
+ npm run build
41
+ ```
42
+
43
+ O servidor compilado inicia por `node dist/server.js`. Ele usa exclusivamente
44
+ `stdio`: não abre porta, não oferece transporte HTTP e nunca recebe credenciais
45
+ AWS. A ferramenta `upload_media` envia multipart para a API Certifica; somente a
46
+ API armazena o arquivo no S3.
47
+
48
+ Crie um cliente de integração na API:
49
+
50
+ ```bash
51
+ npm run api-client:create -- "Claude local" usuario posts:read,posts:write,media:read,media:write
52
+ ```
53
+
54
+ Guarde o token exibido uma única vez e configure o cliente MCP com:
55
+
56
+ - comando: `node`;
57
+ - argumentos: caminho absoluto de `dist/server.js`;
58
+ - `CERTIFICA_API_URL`;
59
+ - `CERTIFICA_API_TOKEN`;
60
+ - `CERTIFICA_API_KEY`, quando exigida pelo ambiente.
61
+
62
+ O fluxo padrão cria `DRAFT`. As ferramentas de agendamento e publicação exigem
63
+ o scope `posts:publish` e o argumento literal `confirm: true`.
64
+
65
+ O servidor carrega `.env` no diretório de trabalho. Em clientes que suportam
66
+ encaminhamento de variáveis, prefira manter os segredos no ambiente do usuário.
@@ -0,0 +1,32 @@
1
+ import type { AppEnv } from "./env.js";
2
+ import { type PostManifest } from "./schemas.js";
3
+ export declare class ApiError extends Error {
4
+ readonly status: number;
5
+ readonly details?: unknown | undefined;
6
+ constructor(message: string, status: number, details?: unknown | undefined);
7
+ }
8
+ export declare class CertificaApi {
9
+ private readonly env;
10
+ private readonly fetchImpl;
11
+ constructor(env: AppEnv, fetchImpl?: typeof fetch);
12
+ private request;
13
+ listPosts(params: Record<string, string | number | undefined>): Promise<unknown>;
14
+ getPost(id: string): Promise<unknown>;
15
+ searchPosts(q: string, page?: number, pageSize?: number): Promise<unknown>;
16
+ listMedia(page?: number, perPage?: number, q?: string): Promise<unknown>;
17
+ validatePost(post: PostManifest): Promise<unknown>;
18
+ validateExistingPost(id: string, expectedVersion: number): Promise<{
19
+ valid: boolean;
20
+ errors: string[];
21
+ warnings: string[];
22
+ stats: {
23
+ images: number;
24
+ words: number;
25
+ };
26
+ }>;
27
+ createDraft(post: PostManifest, idempotencyKey?: string): Promise<unknown>;
28
+ updatePost(id: string, patch: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
29
+ schedulePost(id: string, scheduledFor: string, expectedVersion: number, idempotencyKey?: string): Promise<unknown>;
30
+ publishPost(id: string, expectedVersion: number, idempotencyKey?: string): Promise<unknown>;
31
+ uploadMedia(filePath: string, alt?: string, idempotencyKey?: string): Promise<unknown>;
32
+ }
@@ -0,0 +1,167 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { extname, resolve } from "node:path";
4
+ import { existingPostSchema, validationResultSchema, } from "./schemas.js";
5
+ export class ApiError extends Error {
6
+ status;
7
+ details;
8
+ constructor(message, status, details) {
9
+ super(message);
10
+ this.status = status;
11
+ this.details = details;
12
+ }
13
+ }
14
+ export class CertificaApi {
15
+ env;
16
+ fetchImpl;
17
+ constructor(env, fetchImpl = fetch) {
18
+ this.env = env;
19
+ this.fetchImpl = fetchImpl;
20
+ }
21
+ async request(path, init = {}, idempotencyKey) {
22
+ const headers = new Headers(init.headers);
23
+ headers.set("authorization", `Bearer ${this.env.CERTIFICA_API_TOKEN}`);
24
+ if (this.env.CERTIFICA_API_KEY) {
25
+ headers.set("x-api-key", this.env.CERTIFICA_API_KEY);
26
+ }
27
+ if (idempotencyKey)
28
+ headers.set("idempotency-key", idempotencyKey);
29
+ if (init.body && !(init.body instanceof FormData)) {
30
+ headers.set("content-type", "application/json");
31
+ }
32
+ const url = new URL(path, `${this.env.CERTIFICA_API_URL.replace(/\/$/, "")}/`);
33
+ const requestInit = {
34
+ ...init,
35
+ headers,
36
+ signal: init.signal ?? AbortSignal.timeout(120_000),
37
+ };
38
+ const mayRetry = !init.method || init.method === "GET";
39
+ let response;
40
+ let lastError;
41
+ for (let attempt = 0; attempt < (mayRetry ? 3 : 1); attempt += 1) {
42
+ try {
43
+ response = await this.fetchImpl(url, requestInit);
44
+ if (!mayRetry ||
45
+ (response.status !== 429 && response.status < 500) ||
46
+ attempt === 2) {
47
+ break;
48
+ }
49
+ }
50
+ catch (error) {
51
+ lastError = error;
52
+ if (!mayRetry || attempt === 2)
53
+ throw error;
54
+ }
55
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 150 * 2 ** attempt));
56
+ }
57
+ if (!response)
58
+ throw lastError ?? new Error("A API não respondeu.");
59
+ const body = await response.json().catch(() => null);
60
+ if (!response.ok) {
61
+ const message = body &&
62
+ typeof body === "object" &&
63
+ "message" in body &&
64
+ typeof body.message === "string"
65
+ ? body.message
66
+ : `A API respondeu com HTTP ${response.status}.`;
67
+ throw new ApiError(message, response.status, body);
68
+ }
69
+ return body;
70
+ }
71
+ listPosts(params) {
72
+ const search = new URLSearchParams();
73
+ for (const [key, value] of Object.entries(params)) {
74
+ if (value !== undefined)
75
+ search.set(key, String(value));
76
+ }
77
+ return this.request(`blog/admin/posts?${search}`);
78
+ }
79
+ getPost(id) {
80
+ return this.request(`blog/admin/posts/${id}`);
81
+ }
82
+ searchPosts(q, page = 1, pageSize = 10) {
83
+ const search = new URLSearchParams({
84
+ q,
85
+ page: String(page),
86
+ pageSize: String(pageSize),
87
+ });
88
+ return this.request(`blog/posts/search?${search}`);
89
+ }
90
+ listMedia(page = 1, perPage = 20, q) {
91
+ const search = new URLSearchParams({
92
+ page: String(page),
93
+ perPage: String(perPage),
94
+ });
95
+ if (q)
96
+ search.set("q", q);
97
+ return this.request(`blog/media?${search}`);
98
+ }
99
+ validatePost(post) {
100
+ return this.request("blog/admin/posts/validate", {
101
+ method: "POST",
102
+ body: JSON.stringify(post),
103
+ });
104
+ }
105
+ async validateExistingPost(id, expectedVersion) {
106
+ const post = existingPostSchema.parse(await this.getPost(id));
107
+ if (post.version !== expectedVersion) {
108
+ throw new ApiError(`Conflito de versão. Atual: ${post.version}.`, 409);
109
+ }
110
+ const validation = validationResultSchema.parse(await this.validatePost({
111
+ ...post,
112
+ excerpt: post.excerpt ?? undefined,
113
+ seoTitle: post.seoTitle ?? undefined,
114
+ metaDescription: post.metaDescription ?? undefined,
115
+ focusKeyword: post.focusKeyword ?? undefined,
116
+ visibility: "PUBLIC",
117
+ categoryNames: [],
118
+ tagNames: [],
119
+ }));
120
+ if (!validation.valid) {
121
+ throw new ApiError(`Post inválido: ${validation.errors.join(" ")}`, 422, validation);
122
+ }
123
+ return validation;
124
+ }
125
+ createDraft(post, idempotencyKey = randomUUID()) {
126
+ return this.request("blog/posts", {
127
+ method: "POST",
128
+ body: JSON.stringify({ ...post, status: "DRAFT" }),
129
+ }, idempotencyKey);
130
+ }
131
+ updatePost(id, patch, idempotencyKey = randomUUID()) {
132
+ return this.request(`blog/admin/posts/${id}`, { method: "PATCH", body: JSON.stringify(patch) }, idempotencyKey);
133
+ }
134
+ schedulePost(id, scheduledFor, expectedVersion, idempotencyKey) {
135
+ return this.updatePost(id, { status: "SCHEDULED", scheduledFor, expectedVersion }, idempotencyKey);
136
+ }
137
+ publishPost(id, expectedVersion, idempotencyKey) {
138
+ return this.updatePost(id, { status: "PUBLISHED", expectedVersion }, idempotencyKey);
139
+ }
140
+ async uploadMedia(filePath, alt, idempotencyKey = randomUUID()) {
141
+ const absolutePath = resolve(filePath);
142
+ const fileInfo = await stat(absolutePath);
143
+ if (!fileInfo.isFile())
144
+ throw new Error("O caminho não aponta para um arquivo.");
145
+ if (fileInfo.size === 0)
146
+ throw new Error("O arquivo está vazio.");
147
+ if (fileInfo.size > 25 * 1024 * 1024) {
148
+ throw new Error("A imagem deve ter no máximo 25 MB.");
149
+ }
150
+ const mimeByExtension = {
151
+ ".jpg": "image/jpeg",
152
+ ".jpeg": "image/jpeg",
153
+ ".png": "image/png",
154
+ ".webp": "image/webp",
155
+ };
156
+ const extension = extname(absolutePath).toLowerCase();
157
+ const mimeType = mimeByExtension[extension];
158
+ if (!mimeType)
159
+ throw new Error("Use uma imagem JPEG, PNG ou WebP.");
160
+ const form = new FormData();
161
+ if (alt?.trim())
162
+ form.set("alt", alt.trim());
163
+ form.set("file", new Blob([await readFile(absolutePath)], { type: mimeType }), `upload${extension}`);
164
+ return this.request("blog/media/upload", { method: "POST", body: form }, idempotencyKey);
165
+ }
166
+ }
167
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
package/dist/app.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CertificaApi } from "./api-client.js";
3
+ export declare function createServer(api: CertificaApi): McpServer;
package/dist/app.js ADDED
@@ -0,0 +1,11 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerTools } from "./tools.js";
3
+ export function createServer(api) {
4
+ const server = new McpServer({
5
+ name: "certifica-blog",
6
+ version: "0.1.0",
7
+ });
8
+ registerTools(server, api);
9
+ return server;
10
+ }
11
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,UAAU,YAAY,CAAC,GAAiB;IAC5C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IACH,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ declare const envSchema: z.ZodObject<{
3
+ CERTIFICA_API_URL: z.ZodString;
4
+ CERTIFICA_API_TOKEN: z.ZodString;
5
+ CERTIFICA_API_KEY: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export type AppEnv = z.infer<typeof envSchema>;
8
+ export declare function loadEnv(source?: NodeJS.ProcessEnv): AppEnv;
9
+ export {};
package/dist/env.js ADDED
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ const envSchema = z.object({
3
+ CERTIFICA_API_URL: z.string().url(),
4
+ CERTIFICA_API_TOKEN: z.string().min(20),
5
+ CERTIFICA_API_KEY: z.string().min(1).optional(),
6
+ });
7
+ export function loadEnv(source = process.env) {
8
+ return envSchema.parse(source);
9
+ }
10
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACnC,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IACvC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAIH,MAAM,UAAU,OAAO,CAAC,SAA4B,OAAO,CAAC,GAAG;IAC7D,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ export declare const editorContentSchema: z.ZodObject<{
3
+ format: z.ZodLiteral<"html">;
4
+ version: z.ZodLiteral<1>;
5
+ html: z.ZodString;
6
+ }, z.core.$strip>;
7
+ export declare const postManifestSchema: z.ZodObject<{
8
+ title: z.ZodString;
9
+ slug: z.ZodOptional<z.ZodString>;
10
+ excerpt: z.ZodOptional<z.ZodString>;
11
+ seoTitle: z.ZodOptional<z.ZodString>;
12
+ metaDescription: z.ZodOptional<z.ZodString>;
13
+ focusKeyword: z.ZodOptional<z.ZodString>;
14
+ imageCredit: z.ZodOptional<z.ZodString>;
15
+ content: z.ZodObject<{
16
+ format: z.ZodLiteral<"html">;
17
+ version: z.ZodLiteral<1>;
18
+ html: z.ZodString;
19
+ }, z.core.$strip>;
20
+ coverId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
+ visibility: z.ZodDefault<z.ZodEnum<{
22
+ PUBLIC: "PUBLIC";
23
+ UNLISTED: "UNLISTED";
24
+ PRIVATE: "PRIVATE";
25
+ }>>;
26
+ categoryNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
27
+ tagNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
28
+ }, z.core.$strip>;
29
+ export type PostManifest = z.infer<typeof postManifestSchema>;
30
+ export declare const existingPostSchema: z.ZodObject<{
31
+ content: z.ZodObject<{
32
+ format: z.ZodLiteral<"html">;
33
+ version: z.ZodLiteral<1>;
34
+ html: z.ZodString;
35
+ }, z.core.$strip>;
36
+ title: z.ZodString;
37
+ coverId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
+ id: z.ZodString;
39
+ version: z.ZodNumber;
40
+ excerpt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
+ seoTitle: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ metaDescription: z.ZodOptional<z.ZodNullable<z.ZodString>>;
43
+ focusKeyword: z.ZodOptional<z.ZodNullable<z.ZodString>>;
44
+ }, z.core.$strip>;
45
+ export declare const validationResultSchema: z.ZodObject<{
46
+ valid: z.ZodBoolean;
47
+ errors: z.ZodArray<z.ZodString>;
48
+ warnings: z.ZodArray<z.ZodString>;
49
+ stats: z.ZodObject<{
50
+ images: z.ZodNumber;
51
+ words: z.ZodNumber;
52
+ }, z.core.$strip>;
53
+ }, z.core.$strip>;
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ export const editorContentSchema = z.object({
3
+ format: z.literal("html"),
4
+ version: z.literal(1),
5
+ html: z.string().min(1).max(250_000),
6
+ });
7
+ export const postManifestSchema = z.object({
8
+ title: z.string().min(3).max(160),
9
+ slug: z.string().min(1).max(140).optional(),
10
+ excerpt: z.string().max(300).optional(),
11
+ seoTitle: z.string().max(60).optional(),
12
+ metaDescription: z.string().max(160).optional(),
13
+ focusKeyword: z.string().max(160).optional(),
14
+ imageCredit: z.string().max(300).optional(),
15
+ content: editorContentSchema,
16
+ coverId: z.string().uuid().nullable().optional(),
17
+ visibility: z.enum(["PUBLIC", "UNLISTED", "PRIVATE"]).default("PUBLIC"),
18
+ categoryNames: z.array(z.string().min(1).max(60)).max(20).default([]),
19
+ tagNames: z.array(z.string().min(1).max(60)).max(30).default([]),
20
+ });
21
+ export const existingPostSchema = postManifestSchema
22
+ .pick({
23
+ title: true,
24
+ excerpt: true,
25
+ seoTitle: true,
26
+ metaDescription: true,
27
+ focusKeyword: true,
28
+ content: true,
29
+ coverId: true,
30
+ })
31
+ .extend({
32
+ id: z.string().uuid(),
33
+ version: z.number().int().positive(),
34
+ excerpt: z.string().max(300).nullable().optional(),
35
+ seoTitle: z.string().max(60).nullable().optional(),
36
+ metaDescription: z.string().max(160).nullable().optional(),
37
+ focusKeyword: z.string().max(160).nullable().optional(),
38
+ });
39
+ export const validationResultSchema = z.object({
40
+ valid: z.boolean(),
41
+ errors: z.array(z.string()),
42
+ warnings: z.array(z.string()),
43
+ stats: z.object({
44
+ images: z.number().int(),
45
+ words: z.number().int(),
46
+ }),
47
+ });
48
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/server.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createServer } from "./app.js";
5
+ import { CertificaApi } from "./api-client.js";
6
+ import { loadEnv } from "./env.js";
7
+ async function main() {
8
+ const server = createServer(new CertificaApi(loadEnv()));
9
+ await server.connect(new StdioServerTransport());
10
+ console.error("Certifica MCP iniciado por stdio.");
11
+ }
12
+ main().catch((error) => {
13
+ console.error("Falha ao iniciar Certifica MCP:", error instanceof Error ? error.message : error);
14
+ process.exit(1);
15
+ });
16
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACzD,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;AACrD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CACX,iCAAiC,EACjC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAC/C,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CertificaApi } from "./api-client.js";
3
+ export declare function registerTools(server: McpServer, api: CertificaApi): void;
package/dist/tools.js ADDED
@@ -0,0 +1,143 @@
1
+ import { z } from "zod";
2
+ import { ApiError } from "./api-client.js";
3
+ import { postManifestSchema } from "./schemas.js";
4
+ function result(data) {
5
+ return {
6
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7
+ };
8
+ }
9
+ function errorResult(error) {
10
+ const message = error instanceof ApiError
11
+ ? `${error.message} (HTTP ${error.status})`
12
+ : error instanceof Error
13
+ ? error.message
14
+ : "Erro inesperado.";
15
+ return {
16
+ isError: true,
17
+ content: [{ type: "text", text: message }],
18
+ };
19
+ }
20
+ function safely(handler) {
21
+ return async (...args) => {
22
+ try {
23
+ return result(await handler(...args));
24
+ }
25
+ catch (error) {
26
+ return errorResult(error);
27
+ }
28
+ };
29
+ }
30
+ export function registerTools(server, api) {
31
+ server.registerTool("list_posts", {
32
+ description: "Lista posts administrativos para descoberta e linkagem interna.",
33
+ inputSchema: {
34
+ page: z.number().int().positive().default(1),
35
+ pageSize: z.number().int().min(1).max(100).default(20),
36
+ category: z.string().optional(),
37
+ tag: z.string().optional(),
38
+ },
39
+ annotations: { readOnlyHint: true },
40
+ }, safely(({ page, pageSize, category, tag }) => api.listPosts({ page, pageSize, category, tag })));
41
+ server.registerTool("get_post", {
42
+ description: "Obtém um post administrativo completo pelo UUID.",
43
+ inputSchema: { id: z.string().uuid() },
44
+ annotations: { readOnlyHint: true },
45
+ }, safely(({ id }) => api.getPost(id)));
46
+ server.registerTool("search_posts", {
47
+ description: "Pesquisa posts públicos para linkagem interna.",
48
+ inputSchema: {
49
+ query: z.string().min(1),
50
+ page: z.number().int().positive().default(1),
51
+ pageSize: z.number().int().min(1).max(100).default(10),
52
+ },
53
+ annotations: { readOnlyHint: true },
54
+ }, safely(({ query, page, pageSize }) => api.searchPosts(query, page, pageSize)));
55
+ server.registerTool("list_media", {
56
+ description: "Lista mídias existentes no CMS.",
57
+ inputSchema: {
58
+ page: z.number().int().positive().default(1),
59
+ perPage: z.number().int().min(1).max(100).default(20),
60
+ query: z.string().optional(),
61
+ },
62
+ annotations: { readOnlyHint: true },
63
+ }, safely(({ page, perPage, query }) => api.listMedia(page, perPage, query)));
64
+ server.registerTool("upload_media", {
65
+ description: "Envia uma imagem local JPEG, PNG ou WebP de até 25 MB para a biblioteca Certifica.",
66
+ inputSchema: {
67
+ filePath: z
68
+ .string()
69
+ .min(1)
70
+ .describe("Caminho local absoluto da imagem"),
71
+ alt: z.string().max(200).optional(),
72
+ idempotencyKey: z.string().min(8).max(200).optional(),
73
+ },
74
+ annotations: { readOnlyHint: false, idempotentHint: false },
75
+ }, safely(({ filePath, alt, idempotencyKey }) => api.uploadMedia(filePath, alt, idempotencyKey)));
76
+ server.registerTool("validate_post", {
77
+ description: "Valida conteúdo, SEO, capa, imagens e placeholders antes de salvar.",
78
+ inputSchema: postManifestSchema.shape,
79
+ annotations: { readOnlyHint: true },
80
+ }, safely((post) => api.validatePost(post)));
81
+ server.registerTool("create_post_draft", {
82
+ description: "Cria um rascunho. Esta ferramenta nunca publica nem agenda o post.",
83
+ inputSchema: {
84
+ ...postManifestSchema.shape,
85
+ idempotencyKey: z.string().min(8).max(200).optional(),
86
+ },
87
+ annotations: {
88
+ readOnlyHint: false,
89
+ destructiveHint: false,
90
+ idempotentHint: true,
91
+ },
92
+ }, safely(({ idempotencyKey, ...post }) => api.createDraft(post, idempotencyKey)));
93
+ server.registerTool("update_post", {
94
+ description: "Atualiza um post existente com controle otimista de versão.",
95
+ inputSchema: {
96
+ id: z.string().uuid(),
97
+ expectedVersion: z.number().int().positive(),
98
+ patch: postManifestSchema.partial(),
99
+ idempotencyKey: z.string().min(8).max(200).optional(),
100
+ },
101
+ annotations: {
102
+ readOnlyHint: false,
103
+ destructiveHint: true,
104
+ idempotentHint: true,
105
+ },
106
+ }, safely(({ id, expectedVersion, patch, idempotencyKey }) => api.updatePost(id, { ...patch, expectedVersion }, idempotencyKey)));
107
+ server.registerTool("schedule_post", {
108
+ description: "Agenda um post. Use somente após confirmação explícita do usuário.",
109
+ inputSchema: {
110
+ id: z.string().uuid(),
111
+ expectedVersion: z.number().int().positive(),
112
+ scheduledFor: z.string().datetime(),
113
+ confirm: z.literal(true),
114
+ idempotencyKey: z.string().min(8).max(200).optional(),
115
+ },
116
+ annotations: {
117
+ readOnlyHint: false,
118
+ destructiveHint: true,
119
+ idempotentHint: true,
120
+ },
121
+ }, safely(async ({ id, expectedVersion, scheduledFor, idempotencyKey }) => {
122
+ await api.validateExistingPost(id, expectedVersion);
123
+ return api.schedulePost(id, scheduledFor, expectedVersion, idempotencyKey);
124
+ }));
125
+ server.registerTool("publish_post", {
126
+ description: "Publica um post imediatamente. Exige confirmação explícita do usuário.",
127
+ inputSchema: {
128
+ id: z.string().uuid(),
129
+ expectedVersion: z.number().int().positive(),
130
+ confirm: z.literal(true),
131
+ idempotencyKey: z.string().min(8).max(200).optional(),
132
+ },
133
+ annotations: {
134
+ readOnlyHint: false,
135
+ destructiveHint: true,
136
+ idempotentHint: true,
137
+ },
138
+ }, safely(async ({ id, expectedVersion, idempotencyKey }) => {
139
+ await api.validateExistingPost(id, expectedVersion);
140
+ return api.publishPost(id, expectedVersion, idempotencyKey);
141
+ }));
142
+ }
143
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAgB,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,SAAS,MAAM,CAAC,IAAa;IAC3B,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,MAAM,OAAO,GACX,KAAK,YAAY,QAAQ;QACvB,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,UAAU,KAAK,CAAC,MAAM,GAAG;QAC3C,CAAC,CAAC,KAAK,YAAY,KAAK;YACtB,CAAC,CAAC,KAAK,CAAC,OAAO;YACf,CAAC,CAAC,kBAAkB,CAAC;IAC3B,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CACb,OAAyC;IAEzC,OAAO,KAAK,EAAE,GAAG,IAAO,EAAE,EAAE;QAC1B,IAAI,CAAC;YACH,OAAO,MAAM,CAAC,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,GAAiB;IAChE,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EACT,iEAAiE;QACnE,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YAC/B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SAC3B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KACpC,EACD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,EAAE,CAC3C,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CACjD,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,UAAU,EACV;QACE,WAAW,EAAE,kDAAkD;QAC/D,WAAW,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACtC,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KACpC,EACD,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CACpC,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,gDAAgD;QAC7D,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;SACvD;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KACpC,EACD,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CACnC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,CACvC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EAAE,iCAAiC;QAC9C,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;YACrD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SAC7B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KACpC,EACD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAC1E,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EACT,oFAAoF;QACtF,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,kCAAkC,CAAC;YAC/C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;YACnC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;SACtD;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;KAC5D,EACD,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,CAC3C,GAAG,CAAC,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,cAAc,CAAC,CAC/C,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,WAAW,EACT,qEAAqE;QACvE,WAAW,EAAE,kBAAkB,CAAC,KAAK;QACrC,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KACpC,EACD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CACzC,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,mBAAmB,EACnB;QACE,WAAW,EACT,oEAAoE;QACtE,WAAW,EAAE;YACX,GAAG,kBAAkB,CAAC,KAAK;YAC3B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;SACtD;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;SACrB;KACF,EACD,MAAM,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CACrC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,CACtC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,WAAW,EACT,6DAA6D;QAC/D,WAAW,EAAE;YACX,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;YACrB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;YAC5C,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE;YACnC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;SACtD;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;SACrB;KACF,EACD,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,CACxD,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,eAAe,EAAE,EAAE,cAAc,CAAC,CAClE,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,WAAW,EACT,oEAAoE;QACtE,WAAW,EAAE;YACX,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;YACrB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;YAC5C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YACnC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;YACxB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;SACtD;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;SACrB;KACF,EACD,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,EAAE,EAAE;QACrE,MAAM,GAAG,CAAC,oBAAoB,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC,YAAY,CACrB,EAAE,EACF,YAAY,EACZ,eAAe,EACf,cAAc,CACf,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EACT,wEAAwE;QAC1E,WAAW,EAAE;YACX,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;YACrB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;YAC5C,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;YACxB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;SACtD;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;SACrB;KACF,EACD,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,cAAc,EAAE,EAAE,EAAE;QACvD,MAAM,GAAG,CAAC,oBAAoB,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,eAAe,EAAE,cAAc,CAAC,CAAC;IAC9D,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,222 @@
1
+ # Arquitetura do Certifica MCP
2
+
3
+ ## Contexto
4
+
5
+ O sistema editorial terá duas camadas complementares:
6
+
7
+ - `seo-blog-arquitetura`: regras de redação, SEO, legibilidade, branding,
8
+ imagens, ABNT e orquestração;
9
+ - `certifica-mcp`: ferramentas determinísticas para consultar e alterar dados por
10
+ meio da API.
11
+
12
+ ```text
13
+ Codex / Claude
14
+ |
15
+ | MCP (stdio local)
16
+ v
17
+ certifica-mcp
18
+ |
19
+ | HTTPS + token de integração + Idempotency-Key
20
+ v
21
+ certifica-backend
22
+ |
23
+ +-- PostgreSQL
24
+ +-- S3
25
+ ```
26
+
27
+ O gerador de imagens não faz parte do MCP:
28
+
29
+ ```text
30
+ Skill -> Higgsfield no Claude ou Imagegen no Codex
31
+ -> arquivo da imagem
32
+ -> certifica.upload_media
33
+ -> API
34
+ -> S3
35
+ -> URL/ID
36
+ ```
37
+
38
+ Isso mantém o fluxo independente do fornecedor de geração.
39
+
40
+ ## Limites de responsabilidade
41
+
42
+ ### Skill editorial
43
+
44
+ - transformar pesquisa em artigo;
45
+ - aplicar thresholds de legibilidade;
46
+ - produzir metadados e HTML TinyMCE;
47
+ - consultar links internos pelas ferramentas;
48
+ - criar briefs e prompts de imagem;
49
+ - gerar alt, legenda e fonte;
50
+ - substituir placeholders pelas URLs retornadas;
51
+ - pedir revisão antes de publicar.
52
+
53
+ ### MCP
54
+
55
+ - validar argumentos com schemas estritos;
56
+ - chamar endpoints da API;
57
+ - propagar `Idempotency-Key`;
58
+ - normalizar respostas e erros;
59
+ - limitar tamanho e tipo de arquivos antes do envio;
60
+ - ocultar tokens e detalhes internos;
61
+ - expor capabilities claras ao modelo.
62
+
63
+ ### API
64
+
65
+ - autenticar a identidade da integração;
66
+ - autorizar scopes;
67
+ - validar novamente todos os dados;
68
+ - sanitizar HTML;
69
+ - armazenar posts, relações e mídias;
70
+ - hospedar imagens no S3;
71
+ - calcular métricas de conteúdo;
72
+ - auditar alterações.
73
+
74
+ ## Transporte: somente stdio
75
+
76
+ O cliente inicia um processo local. Logs devem ir para `stderr`; `stdout` fica
77
+ reservado exclusivamente ao protocolo MCP.
78
+
79
+ Vantagens:
80
+
81
+ - configuração simples;
82
+ - nenhuma porta pública;
83
+ - superfície de ataque menor;
84
+ - compatível com distribuição por npm.
85
+
86
+ O projeto não terá Streamable HTTP, SSE, servidor web, OAuth de transporte ou
87
+ porta de escuta. Cada usuário executa sua própria instância local, iniciada pelo
88
+ Claude ou Codex. O MCP local chama `certifica-backend` por HTTPS.
89
+
90
+ ## Ferramentas planejadas
91
+
92
+ | Ferramenta | Scope da API | Mutação | Confirmação |
93
+ | ------------------- | --------------- | ------- | --------------------------- |
94
+ | `list_posts` | `posts:read` | não | não |
95
+ | `get_post` | `posts:read` | não | não |
96
+ | `search_posts` | `posts:read` | não | não |
97
+ | `list_media` | `media:read` | não | não |
98
+ | `upload_media` | `media:write` | sim | implícita no pedido |
99
+ | `validate_post` | `posts:write` | não | não |
100
+ | `create_post_draft` | `posts:write` | sim | implícita no pedido |
101
+ | `update_post` | `posts:write` | sim | mostrar resumo da alteração |
102
+ | `schedule_post` | `posts:publish` | sim | explícita |
103
+ | `publish_post` | `posts:publish` | sim | explícita |
104
+
105
+ Não será criada uma ferramenta genérica como `request_api`. Ferramentas genéricas
106
+ ampliam permissões, dificultam validação e tornam o comportamento do modelo menos
107
+ previsível.
108
+
109
+ ## Contrato editorial
110
+
111
+ O MCP trabalhará com um manifesto intermediário:
112
+
113
+ ```json
114
+ {
115
+ "title": "Título",
116
+ "slug": "titulo",
117
+ "excerpt": "Resumo",
118
+ "seoTitle": "Título SEO",
119
+ "metaDescription": "Descrição",
120
+ "focusKeyword": "palavra-chave",
121
+ "content": {
122
+ "format": "html",
123
+ "version": 1,
124
+ "html": "<p>...</p>"
125
+ },
126
+ "coverId": "uuid",
127
+ "categoryNames": ["Acessibilidade"],
128
+ "tagNames": ["NBR 9050"],
129
+ "visibility": "PUBLIC",
130
+ "status": "DRAFT"
131
+ }
132
+ ```
133
+
134
+ Imagens do corpo já devem estar hospedadas antes de criar o draft. O HTML não pode
135
+ conter `URL-FIGURA-*`, `blob:`, `data:` ou URLs locais.
136
+
137
+ ## Autenticação
138
+
139
+ O `API_KEY` atual identifica a aplicação, mas não é suficiente como identidade de
140
+ automação. A API deverá emitir tokens individuais, armazenados somente como hash,
141
+ com:
142
+
143
+ - cliente;
144
+ - autor associado;
145
+ - scopes;
146
+ - estado ativo/revogado;
147
+ - expiração opcional;
148
+ - último uso.
149
+
150
+ Configuração local esperada:
151
+
152
+ ```text
153
+ CERTIFICA_API_URL=https://api.exemplo.com
154
+ CERTIFICA_API_TOKEN=<segredo individual>
155
+ ```
156
+
157
+ O MCP nunca deve imprimir essas variáveis. Cada usuário recebe uma credencial
158
+ individual para a API.
159
+
160
+ ## Idempotência
161
+
162
+ Uploads, criação, edição, agendamento e publicação receberão uma chave de
163
+ idempotência. A API persistirá:
164
+
165
+ - cliente;
166
+ - chave;
167
+ - operação;
168
+ - hash do payload;
169
+ - status e resposta;
170
+ - expiração.
171
+
172
+ Repetir a mesma chave e o mesmo payload devolve a resposta anterior. Reutilizar a
173
+ chave com outro payload falha.
174
+
175
+ ## Imagens
176
+
177
+ Tipos iniciais: JPEG, PNG e WebP. Limite: 25 MiB.
178
+
179
+ Papéis:
180
+
181
+ - `cover`: preferência por 16:9;
182
+ - `body`: 3:2, panorâmica ou proporção justificada pelo conteúdo.
183
+
184
+ O MCP recebe arquivo local legível ou conteúdo binário suportado pelo transporte,
185
+ chama o endpoint multipart e devolve o recurso `Media`. A API continua responsável
186
+ por assinatura real, metadados, orientação, S3 e compensação.
187
+
188
+ Fonte sugerida para imagem gerada:
189
+
190
+ ```text
191
+ Fonte: Elaborado pelo autor com auxílio do Higgsfield (2026).
192
+ ```
193
+
194
+ O nome da ferramenta deve ser fornecido pelo fluxo gerador; o MCP não deve
195
+ inventá-lo.
196
+
197
+ ## Segurança de publicação
198
+
199
+ - Criar sempre como `DRAFT`.
200
+ - Não combinar criação e publicação na mesma ferramenta.
201
+ - Exigir `posts:publish` para agendar/publicar.
202
+ - Antes de publicar, obter a versão atual e validar conflitos.
203
+ - A API deve aceitar controle otimista por `updatedAt` ou versão.
204
+ - Respostas de mutação devem incluir ID, slug, status e URL de revisão.
205
+ - Registrar cliente, autor, ação, post e horário na auditoria.
206
+
207
+ ## Distribuição
208
+
209
+ Primeiro formato:
210
+
211
+ ```text
212
+ npx -y @certifica/blog-mcp
213
+ ```
214
+
215
+ O pacote publicado deve conter apenas o build, licença e metadados necessários.
216
+ O token fica na configuração do cliente MCP, nunca no pacote.
217
+
218
+ Depois, podem ser oferecidos:
219
+
220
+ - pacote npm privado/público;
221
+ - executável por plataforma;
222
+ - instalador ou atualização automática.
@@ -0,0 +1,98 @@
1
+ # Configurar clientes MCP
2
+
3
+ Compile primeiro:
4
+
5
+ ```powershell
6
+ cd A:\Desenvolvimento\Projetos\Certifica\certifica-mcp
7
+ npm install
8
+ npm run build
9
+ ```
10
+
11
+ O processo usa somente `stdio`. Defina as credenciais no ambiente do processo ou
12
+ em um `.env` no diretório de trabalho do MCP:
13
+
14
+ ```env
15
+ CERTIFICA_API_URL=http://127.0.0.1:3333
16
+ CERTIFICA_API_TOKEN=certifica_TOKEN_INDIVIDUAL
17
+ CERTIFICA_API_KEY=CHAVE_GERAL_DA_API
18
+ ```
19
+
20
+ Nunca versione o `.env`.
21
+
22
+ ## Codex
23
+
24
+ Adicione ao `~/.codex/config.toml` ou ao `.codex/config.toml` de um projeto
25
+ confiável:
26
+
27
+ ```toml
28
+ [mcp_servers.certifica]
29
+ enabled = true
30
+ required = true
31
+ command = "node"
32
+ args = ["A:\\Desenvolvimento\\Projetos\\Certifica\\certifica-mcp\\dist\\server.js"]
33
+ cwd = "A:\\Desenvolvimento\\Projetos\\Certifica\\certifica-mcp"
34
+ startup_timeout_sec = 15.0
35
+ ```
36
+
37
+ Com `cwd` apontando para a pasta do MCP, o servidor carrega o `.env` local. Como
38
+ alternativa, use `env_vars` para encaminhar variáveis já definidas no sistema:
39
+
40
+ ```toml
41
+ env_vars = [
42
+ "CERTIFICA_API_URL",
43
+ "CERTIFICA_API_TOKEN",
44
+ "CERTIFICA_API_KEY",
45
+ ]
46
+ ```
47
+
48
+ Reinicie o Codex e confirme que aparecem dez ferramentas do servidor
49
+ `certifica`.
50
+
51
+ ## Claude Code
52
+
53
+ No Windows, execute:
54
+
55
+ ```powershell
56
+ claude mcp add certifica --scope user -- node A:\Desenvolvimento\Projetos\Certifica\certifica-mcp\dist\server.js
57
+ claude mcp get certifica
58
+ ```
59
+
60
+ Defina as três variáveis no ambiente do usuário antes de iniciar o Claude. Para
61
+ uma configuração de projeto explícita, use `.mcp.json`:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "certifica": {
67
+ "type": "stdio",
68
+ "command": "node",
69
+ "args": [
70
+ "A:\\Desenvolvimento\\Projetos\\Certifica\\certifica-mcp\\dist\\server.js"
71
+ ],
72
+ "env": {
73
+ "CERTIFICA_API_URL": "${CERTIFICA_API_URL:-http://127.0.0.1:3333}",
74
+ "CERTIFICA_API_TOKEN": "${CERTIFICA_API_TOKEN}",
75
+ "CERTIFICA_API_KEY": "${CERTIFICA_API_KEY}"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Defina os segredos no sistema antes de iniciar o Claude. Não grave tokens reais
83
+ em `.mcp.json` compartilhado.
84
+
85
+ ## Verificação
86
+
87
+ 1. Chame `list_media`.
88
+ 2. Chame `validate_post` com um manifesto mínimo.
89
+ 3. Envie uma imagem local com `upload_media` e chave idempotente estável.
90
+ 4. Crie um draft somente depois de a validação retornar `valid: true`.
91
+ 5. Não teste `publish_post` sem um token com `posts:publish` e uma confirmação
92
+ explícita.
93
+
94
+ Com a API local e as variáveis configuradas, execute a verificação não mutável:
95
+
96
+ ```powershell
97
+ npm run smoke
98
+ ```
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "certifica-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Servidor MCP para integrar Codex, Claude e outros clientes compatíveis ao fluxo editorial do blog Certifica.",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "certifica-mcp": "dist/server.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "docs",
13
+ ".env.example",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "dev": "tsx src/server.ts",
18
+ "build": "tsc -p tsconfig.json",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "test": "vitest run",
21
+ "smoke": "tsx scripts/smoke.ts",
22
+ "lint": "eslint .",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check ."
25
+ },
26
+ "keywords": [],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.30.0",
34
+ "dotenv": "^17.4.2",
35
+ "ky": "^2.0.2",
36
+ "zod": "^4.4.3"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^10.0.1",
40
+ "@types/node": "^26.1.2",
41
+ "eslint": "^10.8.0",
42
+ "prettier": "^3.9.6",
43
+ "tsx": "^4.23.1",
44
+ "typescript": "^5.9.3",
45
+ "typescript-eslint": "^8.65.0",
46
+ "vitest": "^4.1.10"
47
+ }
48
+ }