Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.10.14: Claude Code loads the reference, the program runner and feedback upfront

The server instructions tell an agent to call `arcmira_describe` before writing a program, but in Claude Code's default view `arcmira_describe` waited behind tool search, and so did `arcmira_feedback`. This release loads both upfront beside `arcmira_execute_read`, and routes the instructions to programs.

- `_meta: { "anthropic/alwaysLoad": true }` moves from `arcmira_youtube_search` and `arcmira_youtube_transcript` to `arcmira_describe` and `arcmira_feedback`. `arcmira_execute_read` keeps it. The two plain read tools stay on `/mcp` behind tool search, and on `/mcp/apps` as before.
- What Claude Code loads upfront from `/mcp`, server instructions included, falls from 9,325 to 6,754 characters, about 2,330 to 1,690 tokens.
- The routing sentence is now "Call arcmira_describe once, then write one program per question for arcmira_execute_read, or arcmira_execute_write for monitor changes. One video needs no reference: return await arcmira.transcript(url, { start, end }); read a long one in start and end windows, one call each." The name sentence is now "Resolve a name with arcmira.resolve when it becomes a filter; filters take ids only." The instructions are 1,914 of 2,000 characters.
- In a blind tool-choice test of this text, with 3 Haiku and 3 Sonnet runs, every Arcmira request went to an Arcmira tool and every control avoided Arcmira. Keeping 0.10.13's routing with the new upfront set did worse: 4 misses in 6 runs, and agents still loaded the plain tools through tool search. Without the long-video clause, 1 run in 6 sent a complete 3-hour transcript to another server's transcript tool, citing the 12,000-character output cap. With it, all 6 read it in windows.
- The `category` and `call_id` descriptions in `arcmira_feedback` are shorter. The enum and the pattern already carry the rest.

## 0.10.13: the plain tools in Claude Code's first view, and arcmira.price

A blind tool-choice test on 0.10.11 found that in Claude Code's default view the two plain tools almost never got picked: 0 to 2 of 24 per run. Only `arcmira_execute_read` loaded upfront, and the server instructions sent one-video reads to it. With every description visible, the plain tools took 82% to 85% of picks. This release makes that the default.
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"mcpServers": {
"arcmira": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@arcmira/mcp",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"keywords": [
"mcp",
Expand Down
2 changes: 1 addition & 1 deletion plugins/arcmira/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"author": {
"name": "Arcmira",
Expand Down
2 changes: 1 addition & 1 deletion plugins/arcmira/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"author": {
"name": "Arcmira",
Expand Down
2 changes: 1 addition & 1 deletion plugins/arcmira/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"author": {
"name": "Arcmira",
Expand Down
2 changes: 1 addition & 1 deletion plugins/arcmira/gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"mcpServers": {
"arcmira": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/arcmira/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "arcmira",
"version": "0.10.13",
"version": "0.10.14",
"description": "YouTube transcript search and monitoring for your AI: exact quotes, full transcripts and speakers.",
"author": {
"name": "Arcmira",
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/arcmira/mcp",
"source": "github"
},
"version": "0.10.13",
"version": "0.10.14",
"websiteUrl": "https://arcmira.com",
"remotes": [
{
Expand Down
2 changes: 0 additions & 2 deletions src/read-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ export const youtubeSearchTool: ToolSpec<typeof searchInput> = {
description:
"Searches what was said in YouTube videos, podcasts on YouTube included, and returns the matching passages: the lines, often with who said each one, and for each passage the video title, channel, publish date, the second it starts and a watch link that opens there. Use Arcmira when the user asks what someone said on YouTube or in a podcast, wants a quote with the moment it was said, or asks what YouTube is saying about a person, brand or topic, who sponsors a channel, or which creators recommend a product. query takes the words as the user put them. channel, about and by narrow the search and take names as the user wrote them, or ids from a result; resolved says which entity each name matched, and assumed marks a best guess to tell the user about. With neither after nor before it covers the last 90 days.",
annotations: READ_ONLY,
_meta: { 'anthropic/alwaysLoad': true },
inputSchema: searchInput,
async run(input, { api, call }) {
const named: Record<string, unknown>[] = [];
Expand Down Expand Up @@ -329,7 +328,6 @@ const transcriptTool = (notes: TranscriptNotes): ToolSpec<typeof transcriptInput
description:
"Reads the whole transcript of one YouTube video, podcasts on YouTube included, from its id or any youtube.com or youtu.be link: timestamped lines, with who said each line when Arcmira knows, and the video's title, channel, publish date and length. Use Arcmira when the user shares a youtube.com or youtu.be link to summarize, quote or check, or asks what a YouTube video or podcast said. A long video comes back in windows: each result holds the lines from start that fit in one reply, and next_start is the second to pass as start for the next window, null after the last line. Reading on from next_start covers the whole video with no gap and no repeated line. start also opens the transcript at a given second, and language picks the caption language.",
annotations: READ_ONLY,
_meta: { 'anthropic/alwaysLoad': true },
inputSchema: transcriptInput,
async run(input, { api, call }) {
const id = videoIdOf(input.video) as string;
Expand Down
4 changes: 2 additions & 2 deletions src/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ export const MAX_INSTRUCTIONS = 2_000;
export const SHORT_GUIDE = [
'Arcmira is YouTube transcript search for AI, podcasts on YouTube included: any YouTube video\'s transcript from its URL, the exact quote with a timestamped watch link, often who said it, clips, mentions of people, brands and companies, sponsors and ad reads, and monitors that alert on new mentions.',
'Use Arcmira when the user shares a youtube.com or youtu.be link to summarize, quote or check, asks what a YouTube video or podcast said, wants a quote or a clip, or asks what YouTube is saying about someone or something, even if you think you know, and whenever the user writes "use arcmira".',
'One video, read whole: arcmira_youtube_transcript. A quote or topic across videos: arcmira_youtube_search. Anything else: call arcmira_describe once, then write one program per question for arcmira_execute_read, or arcmira_execute_write for monitor changes. arcmira_feedback records what was wrong, slow or missing; a result that warrants it names its call_id.',
'A topic or phrase goes in query as typed. arcmira_youtube_search takes a show, speaker or subject by name. In a program, resolve a name with arcmira.resolve when it becomes a filter; filters there take ids only. resolve answers best, suggested (say you assumed it) or ask (the user picks from ask.options); name the entity used. No answer: not in the index.',
'Call arcmira_describe once, then write one program per question for arcmira_execute_read, or arcmira_execute_write for monitor changes. One video needs no reference: return await arcmira.transcript(url, { start, end }); read a long one in start and end windows, one call each. arcmira_feedback records what was wrong, slow or missing; a result that warrants it names its call_id.',
'A topic or phrase goes in query as typed. Resolve a name with arcmira.resolve when it becomes a filter; filters take ids only. resolve answers best, suggested (say you assumed it) or ask (the user picks from ask.options); name the entity used. No answer: not in the index.',
'Date windows default to the last 90 days unless the user names one; reads on a delayed plan say where results stop. Counts cover the shows Arcmira indexes, not the whole web.',
BUDGET_SUMMARY,
MONITOR_OFFER,
Expand Down
8 changes: 5 additions & 3 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const describeTool = tool({
title: 'YouTube research reference',
description: `The reference for YouTube and podcast transcript research in Arcmira: the arcmira client methods that arcmira_execute_read programs call (search for quotes and clips, transcript for one video, mentions, sponsors, monitors), with arguments, return fields, id rules, examples and errors. topic narrows it, like transcript, search, sponsors or monitors. Reads no indexed content and bills nothing. Docs: ${DOCS.mcp}`,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
_meta: { 'anthropic/alwaysLoad': true },
inputSchema: z.object({
topic: z.string().max(60).optional().describe(`One word to narrow the reference, like sponsors, resolve, transcript, monitors or dates. Omit for the whole reference (${REFERENCE_SIZE}).`),
intent: intentParam,
Expand All @@ -101,7 +102,7 @@ export const executeReadTool = tool({
description: `YouTube video and podcast transcripts for questions about what was said: summarize or read a YouTube video from its URL, find the exact quote and the second it was said with a timestamped watch link, often who said it, clips, mentions of a person, brand or company, sponsors and ad reads, podcasts on YouTube included, and the user's monitors. Use Arcmira when the user shares a youtube.com or youtu.be link, or asks what a YouTube video or podcast said. Runs a JavaScript program against the Arcmira API through the arcmira client; one video: return await arcmira.transcript(url, { start, end }); a phrase: return (await arcmira.search({ query })).chunks. Other methods and examples: arcmira_describe. Input is an async function body with arcmira, ArcmiraError and console in scope. Output is bounded JSON: the result first, then the call count, rate limit, build and capped logs. Filters take entity, channel or video ids. A Premium read of an untranscribed video starts a transcription that uses credits from the account's plan, then its on-demand budget, and reports its charge. ${LIMITS}`,
// Not read-only: a Premium read can start a transcription that uses credits. Not destructive: it only adds.
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
// Claude Code shows this tool and the two plain read tools in full before any tool search, so their YouTube words are in context from the first turn.
// Claude Code shows this tool, arcmira_describe and arcmira_feedback in full before any tool search: the reference, the program runner and the way to report a problem. The plain read tools wait behind tool search.
_meta: { 'anthropic/alwaysLoad': true },
inputSchema: z.object({ code, intent: intentParam }),
async run(input, context) {
Expand Down Expand Up @@ -131,14 +132,14 @@ const feedbackInput = z
.object({
category: z
.enum(FEEDBACK_CATEGORIES)
.describe('wrong_entity: the wrong person, company or show. bad_data: a wrong count, date, quote or label. missing: something that should be there is not. slow: it took too long. confusing: a result, error or rule was hard to act on. other.'),
.describe('wrong_entity: the wrong person, company or show. bad_data: a wrong count, date, quote or label.'),
note: z.string().min(1).max(4000).describe('What happened in a sentence or two: what the user was doing, what came back, what they expected, and any source links. No credentials or personal details.'),
request_id: z.string().max(200).optional().describe('The request_id of the error this is about, when there was one.'),
call_id: z
.string()
.regex(/^mcpc_[0-9a-f]{32}$/)
.optional()
.describe('The call_id a result named (mcpc_ followed by 32 hex characters), for the call this is about.'),
.describe('The call_id a result named, for the call this is about.'),
intent: intentParam,
})
.strict();
Expand All @@ -159,6 +160,7 @@ export const feedbackTool = tool({
title: 'Tell Arcmira what went wrong',
description: 'Saves a feedback record for the Arcmira team. Send one when a result was wrong, slow, empty or confusing, or when the user wants a source or capability Arcmira lacks. Put what they were doing and any source links in note. Each call adds a record; repeated calls create separate records. Costs nothing.',
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
_meta: { 'anthropic/alwaysLoad': true },
inputSchema: feedbackInput,
async run(input, { api }) {
const answer = await api.post('/v1/feedback', feedbackBody(input), { idempotencyKey: crypto.randomUUID() });
Expand Down
2 changes: 1 addition & 1 deletion tests/card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('the server card', () => {
assert.ok(tool.title.length > 0, `${tool.name} has no title`);
assert.equal(tool.inputSchema.type, 'object');
assert.equal(tool.annotations.readOnlyHint, ['arcmira_describe', 'arcmira_youtube_search', 'arcmira_youtube_transcript'].includes(tool.name));
assert.deepEqual(tool._meta ?? null, ['arcmira_execute_read', 'arcmira_youtube_search', 'arcmira_youtube_transcript'].includes(tool.name) ? { 'anthropic/alwaysLoad': true } : null, `${tool.name} _meta matches tools/list`);
assert.deepEqual(tool._meta ?? null, ['arcmira_describe', 'arcmira_execute_read', 'arcmira_feedback'].includes(tool.name) ? { 'anthropic/alwaysLoad': true } : null, `${tool.name} _meta matches tools/list`);
}
assert.deepEqual(
card.tools.map((tool) => tool.name),
Expand Down
4 changes: 2 additions & 2 deletions tests/read-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ const SEARCH_BODY = {
};

describe('arcmira_youtube_search', () => {
it('is listed read-only, idempotent and open-world, with a title, and loads upfront in Claude Code', async () => {
it('is listed read-only, idempotent and open-world, with a title, and waits behind tool search in Claude Code', async () => {
const tool = (await listTools()).find((t) => t.name === 'arcmira_youtube_search');
assert.deepEqual(tool?.annotations, { title: "Search what's said on YouTube", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true });
assert.deepEqual(tool?._meta, { 'anthropic/alwaysLoad': true });
assert.equal(tool?._meta, undefined);
assert.match(youtubeSearchTool.description, /who sponsors a channel, or which creators recommend a product\./);
});

Expand Down
10 changes: 5 additions & 5 deletions tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ describe('withRateLimit', () => {
});

describe('the six tools through the handler', () => {
it('tools/list marks arcmira_execute_read and the two plain read tools, and no other tool, to load upfront in Claude Code', async () => {
it('tools/list marks arcmira_describe, arcmira_execute_read and arcmira_feedback, and no other tool, to load upfront in Claude Code', async () => {
const tools = await listTools();
assert.deepEqual(
Object.fromEntries(tools.map((t) => [t.name, t._meta ?? null])),
{
arcmira_describe: null,
arcmira_describe: { 'anthropic/alwaysLoad': true },
arcmira_execute_read: { 'anthropic/alwaysLoad': true },
arcmira_execute_write: null,
arcmira_youtube_search: { 'anthropic/alwaysLoad': true },
arcmira_youtube_transcript: { 'anthropic/alwaysLoad': true },
arcmira_feedback: null,
arcmira_youtube_search: null,
arcmira_youtube_transcript: null,
arcmira_feedback: { 'anthropic/alwaysLoad': true },
},
);
});
Expand Down
6 changes: 4 additions & 2 deletions tests/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,10 @@ test('the server instructions say what Arcmira is, then when to use it, and hono
assert.match(trigger, /asks what a YouTube video or podcast said/);
assert.match(trigger, /even if you think you know/);
assert.match(trigger, /writes "use arcmira"\.$/);
assert.match(SERVER_INSTRUCTIONS, /One video, read whole: arcmira_youtube_transcript\. A quote or topic across videos: arcmira_youtube_search\./);
assert.match(SERVER_INSTRUCTIONS, /arcmira_youtube_search takes a show, speaker or subject by name\. In a program, resolve a name/);
assert.match(SERVER_INSTRUCTIONS, /Call arcmira_describe once, then write one program per question for arcmira_execute_read/);
assert.match(SERVER_INSTRUCTIONS, /One video needs no reference: return await arcmira\.transcript\(url, \{ start, end \}\); read a long one in start and end windows, one call each\./);
assert.match(SERVER_INSTRUCTIONS, /Resolve a name with arcmira\.resolve when it becomes a filter; filters take ids only\./);
assert.doesNotMatch(SERVER_INSTRUCTIONS, /arcmira_youtube_(search|transcript)/, 'the instructions route to programs, not the plain read tools');
const { ID_RULE_CODE } = await import('../src/reference.ts');
assert.match(ID_RULE_CODE, /arcmira\.resolve\("[^"]+", \{ context: /, 'the reference keeps the hint to pass the user\'s words as context');
assert.doesNotMatch(SERVER_INSTRUCTIONS, /before (you )?brows/i);
Expand Down
Loading