Taxmeter

Prijungti agentą / Connect an agent

Vienas serveris ir OAuth išteklius visiems klientams. / One server and OAuth resource for every client.

Pradžia / First steps

Atidarykite https://app.taxmeter.lt, prisijunkite su Google, priimkite sąlygas, privatumo pranešimą ir DPA. Sukurkite darbo erdvę ir patvirtinkite teisę veikti jos įmonės vardu. Kliente pridėkite https://app.taxmeter.lt/mcp kaip Streamable HTTP su OAuth.

Open https://app.taxmeter.lt, sign in with Google and accept the terms, privacy notice and DPA. Create a workspace and confirm authority to act for its business. Add https://app.taxmeter.lt/mcp in your client as Streamable HTTP with OAuth.

Leidimo ribos / Grant boundaries

Savininkas pasirenka vieną darbo erdvę, mėnesius arba visus mėnesius, modelio teikėją, teises ir 1–30 dienų galiojimą. read būtina. prepare leidžia importuoti ir siūlyti; configure kuria mėnesius; review tikrina; revise keičia peržiūrėtus šaltinius; approve reikia review; export skaito patvirtintą paketą. Kliento įrankių patvirtinimai galioja papildomai.

The owner selects one workspace, selected or all months, model provider, scopes and a 1–30 day expiry. read is required. prepare imports and proposes; configure creates months; review checks; revise changes reviewed sources; approve requires review; export reads approved packages. Client tool permissions apply in addition to the grant.

Claude Code

Terminale vykdykite toliau pateiktas komandas. Pasirinkite tik užduočiai reikalingus tikslius įrankių pavadinimus.

Run these terminal commands. Allow only exact tool names required for the task.

claude mcp add --transport http taxmeter https://app.taxmeter.lt/mcp
claude mcp login taxmeter

Codex

Pridėkite serverį ir prisijunkite. enabled_tools sąrašas ir kiekvieno įrankio approval_mode turi atitikti jūsų užduotį; nenaudokite visų įrankių pakaitos simbolio.

Add the server and sign in. Keep enabled_tools and each tool approval_mode aligned with your task; avoid a tool wildcard.

codex mcp add taxmeter --url https://app.taxmeter.lt/mcp
codex mcp login taxmeter

claude.ai ir Claude Desktop / claude.ai and Claude Desktop

Customize → Connectors → Add connector → Add custom connector. Pavadinimas Taxmeter, URL https://app.taxmeter.lt/mcp. Pasirinkite Sign in now ir paskelbtą Claude kliento tapatybę, tada patvirtinkite Taxmeter leidimą.

Customize → Connectors → Add connector → Add custom connector. Name Taxmeter, URL https://app.taxmeter.lt/mcp. Choose Sign in now and Claude’s published client identity, then approve the Taxmeter grant.

ChatGPT

Plugins → Add → Add custom MCP server. Pavadinimas Taxmeter, URL https://app.taxmeter.lt/mcp, autentifikavimas OAuth. Patvirtinkite kliento rizikos pranešimą, kurkite kaip papildinį ir tęskite į Taxmeter. Paskyros prieinamumas ir meniu gali skirtis.

Plugins → Add → Add custom MCP server. Name Taxmeter, URL https://app.taxmeter.lt/mcp, authentication OAuth. Acknowledge the client notice, create as a plugin and continue to Taxmeter. Account availability and menu wording may vary.

Grok Bot per Cursor / Grok Bot through Cursor

cursor.com/dashboard → Plugins & MCPs → Add → Custom MCP. Pavadinimas taxmeter, tipas URL, serveris https://app.taxmeter.lt/mcp, kliento ID ir paslaptis tušti. Grok Bot: Connect apps → connected → taxmeter → Authenticate. Ryšiai bendri Cursor paskyrai.

cursor.com/dashboard → Plugins & MCPs → Add → Custom MCP. Name taxmeter, type URL, server https://app.taxmeter.lt/mcp; leave client ID and secret empty. In Grok Bot: Connect apps → connected → taxmeter → Authenticate. Connectors are shared across the Cursor account.

Galiojimas ir atkūrimas / Expiry and recovery

Pradėkite agent_access ir pipeline_state. agent.expiresAt yra savininko leidimo pabaiga; agent.accessTokenExpiresAt yra trumpesnio OAuth žetono pabaiga. OAuth klientas pats atnaujina žetonus. Atšaukus leidimą reikia naujo savininko patvirtinimo. LEGAL_ACCEPTANCE_REQUIRED sprendžiama naršyklėje.

Start with agent_access and pipeline_state. agent.expiresAt is the owner grant expiry; agent.accessTokenExpiresAt is the shorter OAuth access-token expiry. The OAuth client refreshes tokens. A revoked grant needs fresh owner authorization. Resolve LEGAL_ACCEPTANCE_REQUIRED in the browser.

Node scenarijus su loopback OAuth / Node script with loopback OAuth

Toliau pateiktas Node 24+ pavyzdys naudoja tik standartinius modulius: aptikimas, dinaminė registracija, PKCE S256, state, iss ir vienas resource. Nukopijuokite į atskirą .mjs failą ir vykdykite. Atidarykite išspausdintą URL naršyklėje. Žetonai lieka atmintyje; neskelbkite jų pokalbyje.

The Node 24+ example below uses built-ins only: discovery, dynamic registration, PKCE S256, state, iss and one resource. Copy it into a separate .mjs file and run it. Open the printed URL in a browser. Tokens remain in memory; never share them in chat.

import { createServer } from 'node:http';
import { createHash, randomBytes } from 'node:crypto';
import { pathToFileURL } from 'node:url';

// Node 24+, built-ins only. Save as oauth-example.mjs outside your repository.
// Run: node oauth-example.mjs. Open the printed authorization URL in your browser.
// Tokens stay in memory. Never paste them into a chat, a URL, source code or a log.
const RESOURCE='https://app.taxmeter.lt/mcp';
const ISSUER='https://app.taxmeter.lt';
async function json(fetcher,url,init){
  const response=await fetcher(url,{...init,redirect:'error',signal:AbortSignal.timeout(15000)});
  if(!response.ok)throw new Error('OAUTH_HTTP_'+response.status);
  return response.json();
}
function endpoint(value,issuer){
  const url=new URL(value);
  if(url.origin!==issuer||url.username||url.password||url.hash)throw new Error('OAUTH_METADATA_INVALID');
  return url.href;
}
export async function connectScript({fetcher=fetch,serverFactory=createServer,origin=ISSUER,
  timeoutMs=180000,ignoreStrayRequests=false,scope='read prepare configure',
  onAuthorizationUrl=url=>{process.stdout.write('Atidarykite / Open: '+url+'\n');}}={}){
  const issuer=new URL(origin);
  if(issuer.protocol!=='https:'||issuer.origin!==origin||issuer.username||issuer.password)throw new Error('OAUTH_ORIGIN_INVALID');
  const resourceUrl=origin+'/mcp';
  const resource=await json(fetcher,origin+'/.well-known/oauth-protected-resource/mcp');
  if(resource.resource!==resourceUrl||!resource.authorization_servers?.includes(origin))throw new Error('OAUTH_METADATA_INVALID');
  const metadata=await json(fetcher,origin+'/.well-known/oauth-authorization-server');
  if(metadata.issuer!==origin||!metadata.code_challenge_methods_supported?.includes('S256'))throw new Error('OAUTH_METADATA_INVALID');
  const authorize=endpoint(metadata.authorization_endpoint,origin),tokenUrl=endpoint(metadata.token_endpoint,origin),registration=endpoint(metadata.registration_endpoint,origin);
  const verifier=randomBytes(32).toString('base64url'),state=randomBytes(32).toString('base64url');
  let settle;const callback=new Promise(resolve=>{settle=resolve;});
  const server=serverFactory((request,response)=>{
    let url;try{url=new URL(request.url,'http://127.0.0.1');}catch{response.writeHead(400);response.end();return;}
    if(ignoreStrayRequests&&url.pathname!=='/callback'){response.writeHead(404,{'Cache-Control':'no-store'});response.end();return;}
    const originForm=request.url.startsWith('/')&&!request.url.startsWith('//');
    const bound=originForm&&request.method==='GET'&&url.pathname==='/callback'&&
      url.searchParams.getAll('state').length===1&&url.searchParams.get('state')===state&&
      url.searchParams.getAll('iss').length===1&&url.searchParams.get('iss')===origin;
    const denied=bound&&url.searchParams.getAll('error').length===1&&url.searchParams.get('error')==='access_denied'&&!url.searchParams.has('code');
    const valid=originForm&&request.method==='GET'&&url.pathname==='/callback'&&
      url.searchParams.getAll('state').length===1&&url.searchParams.get('state')===state&&
      url.searchParams.getAll('iss').length===1&&url.searchParams.get('iss')===origin&&
      url.searchParams.getAll('code').length===1&&Boolean(url.searchParams.get('code'))&&!url.searchParams.has('error');
    response.writeHead(valid?200:400,{'Content-Type':'text/plain; charset=utf-8','Cache-Control':'no-store','Referrer-Policy':'no-referrer','Content-Security-Policy':"default-src 'none'; frame-ancestors 'none'"});
    response.end(valid?'Prisijungta. Grįžkite į scenarijų. / Connected. Return to your script.':'Prisijungimas atmestas. / Sign-in refused.');
    settle(valid?{code:url.searchParams.get('code')}:{error:denied?'OAUTH_ACCESS_DENIED':'OAUTH_CALLBACK_INVALID'});
  });
  let timer;
  try{
    await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
    const redirectUri=`http://127.0.0.1:${server.address().port}/callback`;
    const client=await json(fetcher,registration,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({client_name:'Taxmeter Node script',redirect_uris:[redirectUri],token_endpoint_auth_method:'none',grant_types:['authorization_code','refresh_token'],response_types:['code']})});
    if(typeof client.client_id!=='string'||!client.client_id)throw new Error('OAUTH_REGISTRATION_INVALID');
    const url=new URL(authorize);
    url.search=new URLSearchParams({response_type:'code',client_id:client.client_id,redirect_uri:redirectUri,resource:resourceUrl,scope,state,code_challenge:createHash('sha256').update(verifier).digest('base64url'),code_challenge_method:'S256'});
    timer=setTimeout(()=>settle({error:'OAUTH_CALLBACK_TIMEOUT'}),timeoutMs);
    await onAuthorizationUrl(url.href);
    const result=await callback;if(result.error)throw new Error(result.error);
    const tokens=await json(fetcher,tokenUrl,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'authorization_code',code:result.code,client_id:client.client_id,redirect_uri:redirectUri,resource:resourceUrl,code_verifier:verifier})});
    return {...tokens,clientId:client.client_id,tokenEndpoint:tokenUrl};
  }finally{clearTimeout(timer);server.closeAllConnections();await new Promise(resolve=>server.close(resolve));}
}

// To run: import { connectScript } from './oauth-example.mjs';
// const tokens=await connectScript();
// Use tokens.access_token as a Bearer header for /api/agent/v1 or /mcp.
// On expiry, POST grant_type=refresh_token, refresh_token=tokens.refresh_token,
// client_id=tokens.clientId, resource=RESOURCE to tokens.tokenEndpoint;
// atomically replace BOTH rotated tokens in private storage.
// A revoked/expired grant requires a fresh owner authorization. Never retry writes blindly.
if(process.argv[1]&&pathToFileURL(process.argv[1]).href===import.meta.url){
  await connectScript();
  process.stdout.write('Prisijungta; žetonai nebuvo išspausdinti. / Connected; tokens were not printed.\n');
}