{
  "info": {
    "_postman_id": "395fb3e2-80a9-4e65-bdf3-6037fd9fbdd4",
    "name": "Flux — MCP v1 (OAuth + JSON-RPC)",
    "description": "Fluxo completo do servidor MCP do Flux, requisição por requisição: descoberta, registro dinâmico do cliente, consentimento e chamadas MCP.\n\nUse esta collection para inspecionar o protocolo. Para o uso normal, um cliente de assistente (Claude, ChatGPT, Cursor) faz tudo isso sozinho — basta informar a URL do servidor.\n\nOrdem: rode as pastas 1, 2 e 3 nesta sequência — cada request guarda nas variáveis o que a próxima precisa.\n\nO passo \"Autorização\" é manual por natureza: ele acontece no navegador, com sessão, porque é lá que você entra com a conta do Reportei e escolhe qual conta e quais escopos autorizar. Leia a descrição dele.\n\nDocumentação completa: https://developers.reportei.com/flux",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1 · Descoberta",
      "item": [
        {
          "name": "Desafio: chamada sem credencial (401)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{flux_url}}/mcp/v1",
            "description": "Devolve 401 com WWW-Authenticate apontando o resource metadata. É por este header que o cliente MCP descobre onde autorizar.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"method\": \"tools/list\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('responde 401', () => pm.response.to.have.status(401));",
                  "pm.test('aponta o resource metadata', () => pm.expect(pm.response.headers.get('WWW-Authenticate')).to.include('resource_metadata'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Protected Resource Metadata (RFC 9728)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{flux_url}}/.well-known/oauth-protected-resource",
            "description": "O campo resource casa caractere a caractere com a URL que o cliente digita. authorization_servers aponta o próprio Flux, que é a fachada."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.collectionVariables.set('authorization_server', body.authorization_servers[0]);",
                  "pm.collectionVariables.set('scopes', body.scopes_supported.join(' '));",
                  "pm.test('anuncia o AS', () => pm.expect(body.authorization_servers).to.have.lengthOf(1));"
                ]
              }
            }
          ]
        },
        {
          "name": "Authorization Server Metadata (RFC 8414)",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{authorization_server}}/.well-known/oauth-authorization-server",
            "description": "Sem registration_endpoint aqui, o Claude Code recusa a conexão com 'does not support dynamic client registration'."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.collectionVariables.set('authorize_endpoint', body.authorization_endpoint);",
                  "pm.collectionVariables.set('token_endpoint', body.token_endpoint);",
                  "pm.collectionVariables.set('register_endpoint', body.registration_endpoint);",
                  "pm.test('tem registro dinâmico', () => pm.expect(body.registration_endpoint).to.be.a('string'));",
                  "pm.test('exige S256', () => pm.expect(body.code_challenge_methods_supported).to.include('S256'));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2 · OAuth",
      "item": [
        {
          "name": "Registro dinâmico do cliente (RFC 7591)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{register_endpoint}}",
            "description": "A redirect_uri passa pela allowlist do Flux (loopback, claude.ai, googleusercontent). Fora dela: 400 invalid_redirect_uri.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"redirect_uris\": [\n    \"{{redirect_uri}}\"\n  ],\n  \"client_name\": \"Postman\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('criado', () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set('client_id', pm.response.json().client_id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Autorização: monte a URL e abra no navegador",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{authorize_endpoint}}?client_id={{client_id}}&redirect_uri={{redirect_uri}}&response_type=code&scope={{scopes}}&state=postman&code_challenge={{code_challenge}}&code_challenge_method=S256",
            "description": "ESTE PASSO É MANUAL. A rota exige sessão do navegador: é onde você escolhe a conta e as permissões na tela do Reportei Auth.\n\nRode esta request só para o console imprimir a URL pronta, abra no navegador, autorize, e copie o valor de code= da URL de retorno para a variável 'code' da collection. Aqui dentro do Postman ela só devolve o redirect para o login."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const u = pm.request.url.toString();",
                  "console.log('Abra no navegador:\\n' + pm.variables.replaceIn(u));"
                ]
              }
            }
          ]
        },
        {
          "name": "Token: troca o code pela credencial",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{token_endpoint}}",
            "description": "O code vale 60 segundos e é de uso único. O access_token devolvido é a credencial flux_, a mesma da tela /company/edit/api.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"grant_type\": \"authorization_code\",\n  \"code\": \"{{code}}\",\n  \"client_id\": \"{{client_id}}\",\n  \"redirect_uri\": \"{{redirect_uri}}\",\n  \"code_verifier\": \"{{code_verifier}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('devolveu credencial', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.collectionVariables.set('credential', body.access_token);",
                  "pm.test('é uma chave flux_', () => pm.expect(body.access_token).to.match(/^flux_/));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3 · MCP",
      "item": [
        {
          "name": "initialize (abre a sessão)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{credential}}"
              }
            ],
            "url": "{{flux_url}}/mcp/v1",
            "description": "Guarda o Mcp-Session-Id devolvido no header: toda chamada seguinte precisa dele.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"initialize\",\n  \"params\": {\n    \"protocolVersion\": \"2025-06-18\",\n    \"capabilities\": {},\n    \"clientInfo\": {\n      \"name\": \"postman\",\n      \"version\": \"1.0\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('mcp_session', pm.response.headers.get('Mcp-Session-Id'));",
                  "pm.test('abriu sessão', () => pm.expect(pm.collectionVariables.get('mcp_session')).to.be.a('string'));"
                ]
              }
            }
          ]
        },
        {
          "name": "tools/list",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{credential}}"
              },
              {
                "key": "Mcp-Session-Id",
                "value": "{{mcp_session}}"
              },
              {
                "key": "Mcp-Protocol-Version",
                "value": "2025-06-18"
              }
            ],
            "url": "{{flux_url}}/mcp/v1",
            "description": "O catálogo varia com os escopos do token: o que não foi autorizado não aparece.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"method\": \"tools/list\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('listou tools', () => pm.expect(pm.response.json().result.tools).to.be.an('array'));"
                ]
              }
            }
          ]
        },
        {
          "name": "tools/call · flux_list_projects",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{credential}}"
              },
              {
                "key": "Mcp-Session-Id",
                "value": "{{mcp_session}}"
              },
              {
                "key": "Mcp-Protocol-Version",
                "value": "2025-06-18"
              }
            ],
            "url": "{{flux_url}}/mcp/v1",
            "description": "Erro de tool sai em HTTP 200 com is_error no corpo (ADR-16), não em status HTTP.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 3,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"flux_list_projects\",\n    \"arguments\": {\n      \"per_page\": 5\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Encerrar a sessão",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{credential}}"
              },
              {
                "key": "Mcp-Session-Id",
                "value": "{{mcp_session}}"
              },
              {
                "key": "Mcp-Protocol-Version",
                "value": "2025-06-18"
              }
            ],
            "url": "{{flux_url}}/mcp/v1",
            "description": "Libera a sessão do lado do servidor."
          }
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "flux_url",
      "value": "https://flux.reportei.com",
      "description": "Base do Flux em produção"
    },
    {
      "key": "redirect_uri",
      "value": "http://localhost:8765/callback"
    },
    {
      "key": "code_verifier",
      "value": "flux-mcp-postman-verifier-com-43-caracteres-ok"
    },
    {
      "key": "code_challenge",
      "value": "yBCIeW2gSd4QaLqR-ieiSjNqh-QMOx-ZcSoCKyfDoG4"
    },
    {
      "key": "authorization_server",
      "value": ""
    },
    {
      "key": "authorize_endpoint",
      "value": ""
    },
    {
      "key": "token_endpoint",
      "value": ""
    },
    {
      "key": "register_endpoint",
      "value": ""
    },
    {
      "key": "scopes",
      "value": "projects:read posts:read reviews:read integrations:read"
    },
    {
      "key": "client_id",
      "value": ""
    },
    {
      "key": "code",
      "value": ""
    },
    {
      "key": "credential",
      "value": ""
    },
    {
      "key": "mcp_session",
      "value": ""
    }
  ]
}