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
16 changes: 16 additions & 0 deletions .agents/skills/apa7-campus/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ material proporcionado; cargar las reglas no significa haber leído el trabajo.
En revisiones UPC, la guía institucional usa `s.f.` sin espacios y
`s.f.-a`/`s.f.-b` para distinguir obras sin fecha del mismo autor.

Una consulta con solo `topic` ofrece una vista general y el catálogo, no todas
las reglas detalladas. Sigue `detailLookup.examples` y usa el selector exacto
del tema, uno por llamada. Por ejemplo:

```json
{"topic":"table-figure","tableFigureRuleId":"figure-notes"}
```

El detalle está en `tableFigureRule.rules`; también puedes seleccionar
`figure-components` o `reprinted-adapted-table-figure`. Estos IDs no son
`reviewSectionId` ni `caseId`. Para todas las consideraciones de figuras de la
guía UPC, usa `{"topic":"review","reviewSectionId":"figures"}` y lee
`reviewSkill.considerations.items` y las secciones relacionadas. Son dos vías
distintas: reglas del manual redactadas por Campus y criterios de la guía UPC;
ninguna devuelve una transcripción literal completa del manual.

No hagas el trabajo intelectual del estudiante ni fabriques citas textuales, páginas, DOI, autores o fechas. Puedes explicar, corregir forma y proponer una plantilla con marcadores claros.

## Uso con Blackboard
Expand Down
52 changes: 52 additions & 0 deletions src/providers/academic/apa7-guidance-discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const APA7_GUIDANCE_DESCRIPTION = 'Get Spanish APA 7 guidance and templates. A call with only topic returns an overview and a catalogue, not the detailed rules. Follow detailLookup.examples using the exact selector for that topic. For figure notes call {"topic":"table-figure","tableFigureRuleId":"figure-notes"}; details are in tableFigureRule.rules. For the full UPC figure section call {"topic":"review","reviewSectionId":"figures"}; read reviewSkill.considerations.items and related sections. Review guidance is instructions for the client agent, not a verdict on a document. Does not require Blackboard login; host account authorization still applies.';

export const APA7_TABLE_FIGURE_SELECTOR_DESCRIPTION = 'Only with topic="table-figure": select an id from availableTableFigureRules, e.g. "figure-notes", "figure-components" or "reprinted-adapted-table-figure". Returns the detailed rule in tableFigureRule.rules, plus citation, reference and permission treatment. Do not use reviewSectionId, caseId or a generic ruleId for these rules.';

// Use the catalogue actually returned by this version, rather than promising
// ids absent from the installed package. Also works with the hosted 2.x bundle.
const lookups: Record<string, Array<[string, string, string]>> = {
'principles-ethics': [['principlesEthicsRuleId', 'availablePrinciplesEthicsRules', 'principlesEthicsRule']],
citation: [['citationRuleId', 'availableCitationRules', 'citationRule'], ['caseId', 'availableVerifiedCases', 'case'], ['peruLegalCaseId', 'availablePeruLegalCases', 'legalCase']],
reference: [['referenceRuleId', 'availableReferenceRules', 'referenceRule'], ['caseId', 'availableVerifiedCases', 'case'], ['peruLegalCaseId', 'availablePeruLegalCases', 'legalCase']],
format: [['formatRuleId', 'availableFormatRules', 'formatRule']],
reporting: [['reportingRuleId', 'availableReportingRules', 'reportingRule']],
'writing-style': [['writingStyleRuleId', 'availableWritingStyleRules', 'writingStyleRule']],
'bias-free-language': [['biasFreeLanguageRuleId', 'availableBiasFreeLanguageRules', 'biasFreeLanguageRule']],
mechanics: [['mechanicsRuleId', 'availableMechanicsRules', 'mechanicsRule']],
'table-figure': [['tableFigureRuleId', 'availableTableFigureRules', 'tableFigureRule']],
legal: [['legalRuleId', 'availableLegalRules', 'legalRule'], ['peruLegalCaseId', 'availablePeruLegalCases', 'legalCase']],
publication: [['publicationRuleId', 'availablePublicationRules', 'publicationRule']],
};

export function addApa7DetailDiscovery(topic: string, payload: Record<string, any>) {
const selectors = (lookups[topic] ?? []).filter(([, catalogue]) => payload[catalogue]?.length);
if (topic === 'review' && payload.reviewSkill?.selection === 'all') {
selectors.push(['reviewSectionId', 'reviewSkill.catalog', 'reviewSkill']);
}
if (!selectors.length) return { ...payload, guidanceScope: topic === 'course-requirements' ? 'course-workflow' : 'selected-guidance' };
const examples = selectors.flatMap(([parameter, catalogue, resultField]) => {
const ids: string[] = parameter === 'reviewSectionId'
? payload.reviewSkill.catalog.map((section: { id: string }) => section.id)
: payload[catalogue];
const preferred = topic === 'table-figure'
? ['figure-notes', 'figure-components', 'reprinted-adapted-table-figure']
: topic === 'review' ? ['figures', 'tables'] : [];
const selected = preferred.filter(id => ids.includes(id));
return (selected.length ? selected : ids.slice(0, 1)).map(id => ({
tool: 'campus_apa7_guidance', arguments: { topic, [parameter]: id }, resultField,
}));
});
return {
...payload,
guidanceScope: 'overview',
detailLookup: {
instructions: 'Esta respuesta es una vista general y un catálogo. Para una duda concreta, ejecuta las consultas de ejemplo con el selector exacto; no sustituyas las reglas detalladas por el resumen ni inventes nombres de parámetros. Usa un solo selector por llamada. Las reglas son orientación redactada por Campus, no una transcripción literal del manual.',
selectors: selectors.map(([parameter, availableIdsField, resultField]) => ({ parameter, availableIdsField, resultField })),
examples,
...(topic === 'table-figure' ? {
upcSection: { tool: 'campus_apa7_guidance', arguments: { topic: 'review', reviewSectionId: 'figures' }, resultField: 'reviewSkill.considerations.items' },
warning: 'Que una figura propia no necesite atribución externa no establece por sí solo que deba llevar «Nota. Elaboración propia.»; comprueba las reglas detalladas y las instrucciones reales del docente.',
} : {}),
},
};
}
13 changes: 7 additions & 6 deletions src/providers/academic/apa7-mcp-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { mechanicsRuleId, mechanicsRules, getMechanicsRule, type MechanicsRuleId
import { publicationRuleId, publicationRules, getPublicationRule, type PublicationRuleId } from './apa7-publication-rules.js';
import { principlesEthicsRuleId, principlesEthicsRules, getPrinciplesEthicsRule, type PrinciplesEthicsRuleId } from './apa7-principles-ethics-rules.js';
import { loadApa7ReviewSkill } from './apa7-review-skill.js';
import { addApa7DetailDiscovery, APA7_GUIDANCE_DESCRIPTION, APA7_TABLE_FIGURE_SELECTOR_DESCRIPTION } from './apa7-guidance-discovery.js';

const topic = z.enum(['principles-ethics', 'citation', 'reference', 'format', 'reporting', 'writing-style', 'bias-free-language', 'mechanics', 'table-figure', 'legal', 'publication', 'review', 'course-requirements']);
const sourceType = z.enum([
Expand Down Expand Up @@ -426,8 +427,8 @@ export function registerAcademicTools(
options: { authorize: () => boolean | Promise<boolean> },
) {
server.registerTool('campus_apa7_guidance', {
description: 'Get Spanish APA 7 guidance and templates. For reviewing or correcting a UPC paper, use topic=review: it loads the apa7-review-campus skill with official Biblioteca UPC criteria, source URLs and verification dates for the client agent to apply to the supplied paper. Does not require Blackboard login and never invents metadata.',
inputSchema: {
description: APA7_GUIDANCE_DESCRIPTION,
inputSchema: z.strictObject({
topic: topic.describe('The APA 7 help needed; review loads a source-grounded review skill for the client agent, not an automatic verdict on a document'),
reviewSectionId: z.string().min(1).max(80).optional().describe('With topic=review only: select a UPC section id from reviewSkill.catalog to retrieve all its schemas, considerations, exceptions and conditional safeguards, including notes without examples, plus detailed source evidence'),
sourceType: sourceType.optional().describe('Source type for a reference template; citation requests specialize personal communications and otherwise use caseId or citationRuleId'),
Expand All @@ -439,12 +440,12 @@ export function registerAcademicTools(
writingStyleRuleId: writingStyleRuleId.optional().describe('Verified Spanish-language APA 7 writing-style and grammar rule from chapter 4'),
biasFreeLanguageRuleId: biasFreeLanguageRuleId.optional().describe('Verified APA 7 bias-free language rule from chapter 5, with citation and reference treatment'),
mechanicsRuleId: mechanicsRuleId.optional().describe('Verified Spanish-language APA 7 mechanics rule from chapter 6'),
tableFigureRuleId: tableFigureRuleId.optional().describe('Verified APA 7 table/figure rule from chapter 7, including citation, reference and permission treatment'),
tableFigureRuleId: tableFigureRuleId.optional().describe(APA7_TABLE_FIGURE_SELECTOR_DESCRIPTION),
legalRuleId: legalRuleId.optional().describe('Verified APA 7 legal-reference rule from chapter 11; jurisdiction-specific and never transferable by analogy'),
peruLegalCaseId: peruLegalCaseId.optional().describe('Peruvian legal-source profile with citation, reference, required metadata and official verification sources'),
publicationRuleId: publicationRuleId.optional().describe('Verified APA 7 publication, copyright, permission and post-publication rule from chapter 12'),
principlesEthicsRuleId: principlesEthicsRuleId.optional().describe('Verified APA 7 writing, publication-ethics and professional rule from chapter 1'),
},
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ topic, reviewSectionId, sourceType, caseId, citationRuleId, referenceRuleId, formatRuleId, reportingRuleId, writingStyleRuleId, biasFreeLanguageRuleId, mechanicsRuleId, tableFigureRuleId, legalRuleId, peruLegalCaseId, publicationRuleId, principlesEthicsRuleId }) => {
if (!options?.authorize) {
Expand All @@ -458,11 +459,11 @@ export function registerAcademicTools(
const hasSelectedRule = Object.values(selectors).some(value => value !== undefined);
const reviewSkill = topic === 'review' ? await loadApa7ReviewSkill(reviewSectionId) : undefined;
return {
content: [{ type: 'text', text: JSON.stringify({
content: [{ type: 'text', text: JSON.stringify(addApa7DetailDiscovery(topic, {
...guidanceFor(topic, sourceType, caseId, citationRuleId, referenceRuleId, formatRuleId, reportingRuleId, writingStyleRuleId, biasFreeLanguageRuleId, mechanicsRuleId, tableFigureRuleId, legalRuleId, peruLegalCaseId, publicationRuleId, principlesEthicsRuleId),
...(reviewSkill ? { sourceGuide: reviewSkill.rulebook.entryUrl, reviewSkill } : {}),
...(hasSelectedRule ? {} : availableSelectorsForTopic(topic)),
}) }],
})) }],
};
});
}
55 changes: 55 additions & 0 deletions tests/apa7-detail-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { registerAcademicTools } from '../src/providers/academic/apa7-mcp-tools.js';

test('MCP overview examples retrieve actual detailed rules for every topic', async () => {
const server = new McpServer({ name: 'apa-detail', version: '1' });
let entitled = true;
registerAcademicTools(server, { authorize: () => entitled });
const client = new Client({ name: 'apa-detail-client', version: '1' });
const [a, b] = InMemoryTransport.createLinkedPair();
await server.connect(a);
await client.connect(b);
const call = async (args: Record<string, unknown>) => {
const result = await client.callTool({ name: 'campus_apa7_guidance', arguments: args });
assert.ok(!result.isError, JSON.stringify(result));
return JSON.parse((result.content as Array<{ text: string }>)[0].text);
};
try {
const { tools } = await client.listTools();
const tool = tools.find(tool => tool.name === 'campus_apa7_guidance')!;
assert.match(tool.description!, /tableFigureRuleId.*figure-notes/);
assert.equal(tool.inputSchema.additionalProperties, false);
for (const topic of ['principles-ethics', 'citation', 'reference', 'format', 'reporting', 'writing-style', 'bias-free-language', 'mechanics', 'table-figure', 'legal', 'publication', 'review']) {
const overview = await call({ topic });
assert.equal(overview.guidanceScope, 'overview', topic);
assert.ok(overview.detailLookup.examples.length, topic);
for (const example of overview.detailLookup.examples) {
const detail = await call(example.arguments);
assert.equal(detail.guidanceScope, 'selected-guidance');
assert.ok(detail[example.resultField], example.resultField);
assert.equal(detail.detailLookup, undefined);
if (topic === 'table-figure') assert.ok(detail.tableFigureRule.rules.length > 1);
if (topic === 'review') assert.ok(detail.reviewSkill.considerations.items.length);
}
if (topic === 'table-figure') {
assert.deepEqual(overview.detailLookup.examples.map((r: any) => r.arguments.tableFigureRuleId), ['figure-notes', 'figure-components', 'reprinted-adapted-table-figure']);
const upc = await call(overview.detailLookup.upcSection.arguments);
assert.equal(upc.reviewSkill.selection, 'figures');
}
}
const typo = await client.callTool({ name: tool.name, arguments: { topic: 'table-figure', ruleId: 'figure-notes' } });
assert.equal(typo.isError, true, 'an unknown selector must not silently return the summary');
const incompatible = await client.callTool({ name: tool.name, arguments: { topic: 'review', tableFigureRuleId: 'figure-notes' } });
assert.equal(incompatible.isError, true);
entitled = false;
const denied = await client.callTool({ name: tool.name, arguments: { topic: 'table-figure', tableFigureRuleId: 'figure-notes' } });
assert.equal(denied.isError, true);
} finally {
await client.close();
await server.close();
}
});
Loading