Skip to main content
Glama
ticket-precedent

ticket-precedent

by bondie

ticket-precedent

CI Python License

Have we solved this before? Local hybrid search over your JIRA and helpdesk history, exposed to AI agents through the Model Context Protocol.

A customer reports "the invoice never showed up in the shop". Eight months ago a developer fixed the same thing in a ticket titled "ERP comparator drops payload as unchanged". Keyword search cannot connect the two, and pure embeddings fumble exact identifiers such as ticket keys, class names and error codes. ticket-precedent indexes tickets and their comments from JIRA and MantisBT into a single SQLite file and ranks them with BM25 and vector search fused by Reciprocal Rank Fusion. Embeddings are computed locally with Ollama, so ticket content never leaves your machine.

In Claude Code (or any MCP client) you ask "find precedents for helpdesk 18342", and the agent gets the earlier tickets that look like it. The command line gives the same results (illustrative output):

$ uv run tp precedents 18342
precedents for [acme-helpdesk] 18342 Invoice missing in the shop
1. [acme-jira] SHOP-412 ERP comparator drops payload as unchanged
   Done Fixed · 3.5 h · https://jira.example.com/browse/SHOP-412
   Root cause: the comparator hashed the payload without the PDF, so the re-sent invoice …
2. [acme-helpdesk] 17105 Credit note not visible in the customer account
   closed fixed · https://helpdesk.example.com/view.php?id=17105
   The ERP export finished but the shop kept the old document list. Resetting the …

Getting started

You need uv and Ollama.

1. Start Ollama and download the embedding model. On macOS:

brew install ollama && brew services start ollama   # starts now and at every login
ollama pull bge-m3                                   # ~1.2 GB, multilingual

On Linux the official installer sets up a systemd service.

2. Install ticket-precedent.

git clone https://github.com/bondie/ticket-precedent && cd ticket-precedent
uv sync

3. Describe your trackers. The configuration lives outside the repository:

mkdir -p ~/.config/ticket-precedent
cp config.example.yaml ~/.config/ticket-precedent/config.yaml

Edit the sources list, with one entry per JIRA query or MantisBT project set. Keep the secrets out of the file: ${VARIABLES} are resolved from the environment, from a .env next to the config, or from the files listed in env_files. See config.example.yaml for every option.

4. Build the index.

uv run tp sync
uv run tp stats            # tickets and chunks per source, last sync time

The first sync fetches everything and embeds about 15 chunks per second on an Apple M1 Pro. A project with 3,600 tickets and their comments (17,000 chunks) takes about 20 minutes. Later syncs fetch only tickets updated since the previous one and embed only changed text. The index is a single file in ~/.local/share/ticket-precedent/.

5. Connect it to Claude Code. Run this in the repository directory:

claude mcp add ticket-precedent -s user -- uv --directory "$PWD" run tp serve

-s user makes the tools available in every project. New Claude Code sessions pick them up, and claude mcp list shows whether the server connects. Other MCP clients (Claude Desktop, Cursor, …) start the same stdio server:

{
  "mcpServers": {
    "ticket-precedent": {
      "command": "uv",
      "args": ["--directory", "/path/to/ticket-precedent", "run", "tp", "serve"]
    }
  }
}

Related MCP server: jira-mcp

Everyday use

Ask your agent

Describe the problem in your own words and let the agent pick the tool:

You ask

The agent calls

Find precedents for helpdesk 18342.

precedents_for("18342")

Have we seen "invoice PDF missing although ERP exported it" before?

similar_tickets(text)

Open SHOP-412 and tell me how it was fixed.

get_ticket("acme-jira", "SHOP-412")

How long did similar fixes take? Only look in JIRA.

precedents_for(..., only_source="acme-jira") and reads time_spent_hours

Tool

What it does

precedents_for(key, source?, only_source?, limit?)

Precedents for a ticket that is already indexed

similar_tickets(text, source?, project?, limit?)

Precedents for any text: a symptom, an error message, a ticket that is not indexed yet

get_ticket(source, key)

Full indexed description, comments and links of one ticket

list_sources()

Indexed sources, counts and last sync time

precedents_for queries with the ticket's title and description, exactly like the benchmark. It returns only tickets created before that ticket and leaves out the ticket itself. It also leaves out tickets named in the ticket's title: a JIRA issue titled "[R:18342] Wrong prices" was opened from helpdesk thread 18342, so that thread is the same problem, not a precedent. The key alone is enough. source is needed only when two sources share a key.

Every result carries the key, title, URL, status, resolution, creation date, time spent and a snippet of the best-matching text. matched_in says whether that text is the description, a comment or only the title. score is the fusion score: it orders the results and is not a probability. All tools are read-only and read only the local index.

Use the command line

Command

What it does

tp precedents KEY [--only-source S] [--limit N] [--json]

Earlier tickets that look like an indexed ticket

tp search TEXT [--source S] [--project P] [--mode hybrid|vector|bm25] [--json]

Search by free text

tp show SOURCE KEY

Print one indexed ticket as JSON

tp sync [--source S] [--full]

Fetch new and updated tickets; --full re-reads everything

tp stats

Indexed sources and sync times

tp serve

MCP server on stdio (MCP clients start it for you)

tp eval PROJECT

Benchmark on a public Apache JIRA project

Run them as uv run tp … from the repository directory. tp --config PATH … uses another configuration file.

Keep the index fresh

  • Automatically. With auto_sync_hours set, every new MCP session checks the sources when it starts. Any source older than that limit is refreshed in a background thread, so no scheduler is needed. A file lock keeps concurrent syncs (CLI, several open sessions) from colliding.

  • On demand. tp sync, or a cron job.

  • After changing chunking settings, for example raising min_comment_chars to drop short "deployed, please check" comments, run tp sync --full. Unchanged chunks keep their embeddings, so this costs mostly API calls.

What has to be running

  • The MCP server is started by the client (Claude Code, Cursor, …) for each session and stops with it. There is no daemon to manage.

  • Ollama embeds the query. An idle Ollama server uses about 70 MB of RAM. The model is loaded on the first query (a few seconds) and unloaded after five idle minutes. If Ollama is not reachable, search degrades to BM25 and says so in a warning field instead of failing.

Results

The benchmark asks the tool to find duplicates on public Apache JIRA projects (see the methodology). Embeddings come from bge-m3 through Ollama on an Apple M1 Pro. The fusion parameters were tuned on ZooKeeper and applied to Derby unchanged.

ZooKeeper: 4,922 tickets, 105 duplicate queries

Method

Indexed text

Hit@1

Hit@5

Hit@10

MRR@10 (95% CI)

BM25

title + description

0.467

0.648

0.686

0.538 (0.45-0.63)

Vector

title + description

0.600

0.762

0.790

0.662 (0.58-0.74)

Hybrid

title + description

0.619

0.771

0.800

0.671 (0.59-0.75)

BM25

+ comments

0.562

0.724

0.781

0.638 (0.55-0.72)

Vector

+ comments

0.610

0.829

0.838

0.695 (0.62-0.77)

Hybrid

+ comments

0.648

0.838

0.857

0.719 (0.64-0.80)

Derby (held out): 7,169 tickets, 153 duplicate queries

Method

Indexed text

Hit@1

Hit@5

Hit@10

MRR@10 (95% CI)

BM25

title + description

0.418

0.595

0.673

0.500 (0.43-0.57)

Vector

title + description

0.490

0.634

0.699

0.551 (0.48-0.62)

Hybrid

title + description

0.490

0.654

0.725

0.559 (0.49-0.63)

BM25

+ comments

0.425

0.667

0.732

0.532 (0.46-0.60)

Vector

+ comments

0.556

0.752

0.791

0.636 (0.57-0.70)

Hybrid

+ comments

0.562

0.752

0.797

0.647 (0.58-0.71)

What the numbers say:

  • Indexing comments gives the biggest gain. With comments, hybrid Hit@10 rises by 6 to 7 points and MRR by 0.05 to 0.09. Comments hold the diagnosis (stack traces, class names, the root cause) that the original description often lacks.

  • Embeddings beat BM25 on every run, by 0.05 to 0.12 MRR. Duplicates are reported by different people in different words.

  • Hybrid is the best or tied on every run, but the right BM25 weight depends on the project. On ZooKeeper without comments, plain RRF (k=60, equal weights) scored 0.599 MRR, which is below vectors alone (0.662). With rrf_k=10 and the lexical ranking weighted 0.25, it reaches 0.671. Derby was not used for tuning, and it prefers a heavier BM25 weight: with comments, equal weights reach 0.661 and the default reaches 0.647. The default of 0.25 is the conservative choice, because it scored above vector search on all four runs. Neither these differences nor hybrid's lead over pure vector search (0.01 to 0.02 MRR) are larger than the confidence intervals, so the benchmark does not prove that hybrid is better. Hybrid stays the default because exact identifiers (error codes, ticket keys, class names) are where BM25 is strong and embeddings are weak, and this benchmark does not isolate that case.

  • Latency. At p50, searching the local index takes 15 to 50 ms without comments. With comments it takes up to 212 ms (hybrid over Derby's 61k chunks). Embedding the query adds about 0.2 s (1,000 characters, model already loaded).

Reproduce a run, or the fusion parameter grid, with:

uv run tp eval ZOOKEEPER --no-comments       # ~5k tickets, about 10 minutes
uv run tp eval DERBY --out results/derby-comments-bge-m3.json
uv run python scripts/tune_fusion.py ~/.local/share/ticket-precedent/eval/apache-derby-comments-bge-m3.db

The per-query ranks for every run are in results/.

How it works

flowchart LR
    J[JIRA] --> C[Connectors<br/>incremental by `updated`]
    M[MantisBT] --> C
    C --> K[Chunker<br/>title-prefixed windows,<br/>bot comments dropped]
    K -->|changed chunks only| E[Ollama embeddings<br/>bge-m3, 1024-d]
    K --> F[(FTS5<br/>BM25)]
    E --> V[(sqlite-vec<br/>cosine KNN)]
    Q[Query] --> F
    Q --> V
    F --> R[Ticket-level<br/>RRF fusion]
    V --> R
    R --> MCP[MCP server<br/>precedents_for · similar_tickets]
    R --> CLI[tp precedents · tp search]
  • One file, no services. FTS5 and sqlite-vec live in the same SQLite database as the tickets, so the index is easy to back up, move or throw away.

  • Hybrid ranking. BM25 catches exact tokens (error codes, class names), embeddings catch paraphrases and cross-language matches. Reciprocal Rank Fusion merges the two rankings without calibrating their scores against each other.

  • Ticket-level results. Every chunk carries the ticket title. Chunks are ranked first and then collapsed to their best chunk per ticket, so a long thread cannot crowd out the list.

  • Incremental sync. Chunks are content-hashed and only new or changed text is embedded. Re-syncing an unchanged project costs API calls but no GPU time.

  • Long queries. A pasted ticket can have hundreds of terms. The lexical side keeps the 64 rarest terms that exist in the index, using FTS5 vocabulary statistics.

  • Multilingual. bge-m3 handles mixed Czech and English. BM25 folds diacritics, so objednávka and objednavka match.

Evaluation methodology

There is no labelled "precedent" dataset for private trackers, but public issue trackers contain one implicitly: tickets closed as duplicates.

  • Corpus: every ticket of an Apache JIRA project, fetched anonymously through the REST API.

  • Queries: each ticket resolved as Duplicate, using only its title and description (truncated to 2,000 characters), which is what a reporter writes before anyone answers.

  • Relevant answers: the earlier tickets in the same duplicate cluster (connected component of Duplicate links).

  • No leakage from the future: search is restricted to tickets created before the query, and the query ticket itself is excluded.

  • Metrics: Hit@k (a relevant ticket is in the top k) and MRR@10. The 95% interval for MRR is a percentile bootstrap: 2,000 resamples of the queries with a fixed seed.

Caveat: the index reflects the trackers' current state, so a comment written on an original ticket after the duplicate was filed can still help the search. Titles and descriptions, the bulk of the signal, rarely change after creation.

Development

uv sync
uv run pytest --cov          # unit and integration tests, no network or GPU needed
uv run ruff check . && uv run ruff format --check .
uv run mypy                  # strict mode
uvx pre-commit install       # ruff, mypy and gitleaks on every commit

CI runs lint, type checks, tests on Python 3.11–3.13 and a gitleaks secret scan.

Roadmap

  • Collapse clones and sub-tasks that share a title into one result

  • Cross-encoder reranking of the top candidates (bge-reranker-v2-m3)

  • Embedding model comparison (qwen3-embedding, multilingual-e5, snowflake-arctic-embed2)

  • GitLab and GitHub Issues connectors, JIRA Cloud (/rest/api/3/search/jql)

  • Periodic re-sync while a server session stays open (today auto-sync runs only at start-up)

License

MIT

Available Tools

4 tools
get_ticketC
Read-only

Full indexed text of one ticket: description, comments, links and time spent.

NameRequiredDescriptionDefault
keyYes
sourceYes

Output Schema

NameRequiredDescription

No output parameters

TDQS

C2.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, so safety is covered, and the description adds real value by listing the sections returned in the indexed text. It says nothing about behavior when the key is unknown, permission requirements, or result size, which matter for a full-text fetch.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with the payload contents enumerated; nothing is wasted. It is efficient but so terse that the missing parameter explanation is not compensated elsewhere.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described, but with two required, zero-coverage parameters the definition leaves an agent unable to determine what "source" accepts. For a lookup tool whose correctness depends entirely on those inputs, this is a significant gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must carry the burden, yet it explains neither parameter. "source" in particular is opaque (which tracker/system?) and no key format is given, leaving both required parameters semantically undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

"Full indexed text of one ticket" states a specific resource and scope, and the enumeration of description, comments, links and time spent tells the agent exactly what content comes back. It does not name or contrast itself with the sibling similar_tickets, so the distinction has to be inferred.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no when-to-use or when-not-to-use guidance and no mention of the alternatives (similar_tickets, precedents_for, list_sources). An agent must guess that this is the single-ticket retrieval path versus the related-ticket paths from the sibling names alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_sourcesC
Read-only

Indexed sources with ticket and chunk counts and the last sync time.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
resultYes

TDQS

C2.8/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. Beyond that, the description adds no behavioral context such as result ordering, completeness, or whether the sync time reflects failed syncs. Its content is largely restated by the existing output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is a single short clause with no wasted words, but it is a fragment that omits the action verb and therefore does not front-load the tool's purpose. Brevity here comes at the cost of specification.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Because an output schema exists, the description need not enumerate return fields, yet it spends its only sentence doing roughly that instead of stating the operation. For a zero-parameter read tool the definition is minimally sufficient, but a single clause like 'List all indexed sources and their sync status' would close the gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is nothing for the description to disambiguate. Baseline 4 applies for a parameterless call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names the resource ('Indexed sources') and the data attached to it, but it is a noun phrase with no verb, so an agent must infer from the tool name that this lists sources. It does not differentiate itself from the sibling ticket tools beyond the obvious subject difference.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to call this versus the sibling tools (similar_tickets, precedents_for, get_ticket), and no prerequisites or exclusions are stated. The only implied usage is discovery of what content is indexed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

precedents_forA
Read-only

Earlier tickets resembling an indexed ticket, e.g. helpdesk thread 18342 or PROJ-42.

    Prefer this to similar_tickets whenever the user names a ticket. The ticket's title and
    description are the query, and only tickets created before it are returned, without
    the ticket itself and tickets its title names. Pass `source` only when the key exists
    in several sources; `only_source` restricts the results to one source.
    
NameRequiredDescriptionDefault
keyYes
limitNo
sourceNo
only_sourceNo

Output Schema

NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds meaningful behavior beyond the read-only annotations: the ticket title and description form the query, only earlier-created tickets are returned, and the ticket itself plus title-named tickets are excluded. The phrase 'tickets its title names' is slightly ambiguous, but the core non-obvious constraints are disclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded, appropriately sized, and every sentence contributes useful routing or behavioral information. The exclusion phrase is a bit densely worded, but there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema and annotations, the description covers the core retrieval semantics and sibling routing well. The main missing piece is limit behavior, which should be clarified because the schema provides no description for it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the burden. It explains key, source, and only_source well, but omits any explanation of limit, leaving one of four parameters undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific action: find earlier tickets resembling a given indexed ticket, with examples like helpdesk thread 18342 or PROJ-42. It also explicitly distinguishes itself from the sibling similar_tickets, so an agent can identify its role without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit routing guidance: prefer this to similar_tickets whenever the user names a ticket. It also explains when to pass source versus only_source, leaving little ambiguity about usage conditions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

similar_ticketsA
Read-only

Find past tickets similar to text (a symptom, error message or ticket body).

    Optionally restrict to one `source` (see list_sources) or `project`.
    Returns `results` with key, title, url, status, resolution, time spent and the best
    matching snippet, plus a `warning` when only keyword search was available.
    
NameRequiredDescriptionDefault
textYes
limitNo
sourceNo
projectNo

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the read-only, closed-world safety profile, and the description adds genuinely new behavior: the result fields returned and, importantly, a `warning` when only keyword search was available, which tells the agent results may be weaker than semantic matches. It doesn't mention ranking, thresholds, or what happens when nothing matches.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, front-loaded with the core action, then optional filters, then return shape. No filler; the only mild redundancy is describing return fields that an output schema already declares.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a four-parameter read-only search tool with an output schema, this covers the essential invocation semantics and the degradation caveat. The residual gap is sibling routing (when this beats precedents_for) and the undocumented `limit` parameter.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description must carry the load: it explains `text` (symptom, error message, ticket body) and that `source` and `project` are optional restrictions, and routes to list_sources for source values. It says nothing about `limit` (default 5) or the expected format of `source`/`project`, leaving half the parameters underspecified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Find past tickets similar to `text`') and clarifies what `text` should contain (symptom, error message, ticket body). This is clearly distinct from get_ticket (single lookup) and list_sources, though it never mentions precedents_for, the closest conceptual sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the framing — paste a symptom or error message to find prior occurrences — and it points to list_sources for valid `source` values. However, it never says when to prefer this over precedents_for or get_ticket, nor when the keyword-only fallback makes results unreliable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv0.1.0
    • First observedget_ticket
    • First observedlist_sources
    • First observedprecedents_for
    • First observedsimilar_tickets

TDQS

B3.4/5.0

Scored across 4 tools

Disambiguation4/5

similar_tickets and precedents_for both retrieve prior tickets, but the descriptions clearly distinguish free-text search from ticket-key-driven precedent lookup. get_ticket and list_sources are cleanly separate, so agents should usually pick correctly despite the overlap.

Naming Consistency3/5

The names are readable and all snake_case, but conventions are mixed: get_ticket and list_sources are verb_noun, similar_tickets is adjective_noun, and precedents_for is noun_preposition. There is no single predictable pattern across the four tools.

Tool Count5/5

Four tools is well-scoped for a ticket precedent retrieval server: source discovery, text similarity search, ticket-relative precedent lookup, and full ticket retrieval. Each tool has a clear role and none feels redundant.

Completeness4/5

The surface covers source discovery, both major search modes, and full ticket retrieval, which is solid for the apparent retrieval-focused domain. A minor gap is the lack of an explicit index refresh/sync operation or project listing, though list_sources partly mitigates this.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers