Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { ToolRequestContext } from '@supabase/mcp-utils';
import { describe, expect, test } from 'vitest';

import { resolveElicitationAvailability } from './capability.js';

type Context = Pick<
ToolRequestContext,
'era' | 'clientInfo' | 'clientCapabilities'
>;

const modern = (
clientCapabilities: ToolRequestContext['clientCapabilities']
): Context => ({
era: 'modern',
clientInfo: { name: 'test-client', version: '1.2.3' },
clientCapabilities,
});

const servingPath = { formDeliveryAvailable: true };

describe('form elicitation availability', () => {
test('accepts a mode-less declaration on a supported serving path', () => {
expect(
resolveElicitationAvailability(modern({ elicitation: {} }), servingPath)
).toStrictEqual({ formElicitation: true, reason: 'available' });
});

test('accepts an explicit form declaration on a supported serving path', () => {
expect(
resolveElicitationAvailability(
modern({ elicitation: { form: {} } }),
servingPath
)
).toStrictEqual({ formElicitation: true, reason: 'available' });
});

test('treats URL-only and absent declarations as incapable', () => {
expect(
resolveElicitationAvailability(
modern({ elicitation: { url: {} } }),
servingPath
)
).toStrictEqual({ formElicitation: false, reason: 'capability' });
expect(
resolveElicitationAvailability(modern({}), servingPath)
).toStrictEqual({ formElicitation: false, reason: 'capability' });
});

test('reports an unsupported serving path and an injected opt-out apart', () => {
expect(
resolveElicitationAvailability(modern({ elicitation: {} }), {
formDeliveryAvailable: false,
})
).toStrictEqual({ formElicitation: false, reason: 'serving_path' });
expect(
resolveElicitationAvailability(modern({ elicitation: {} }), {
formDeliveryAvailable: true,
optOut: true,
})
).toStrictEqual({ formElicitation: false, reason: 'opt_out' });
});

test('keeps the classic era incapable even when it declares form support', () => {
const classic: Context = {
era: 'legacy',
clientInfo: { name: 'test-client', version: '1.2.3' },
clientCapabilities: { elicitation: { form: {} } },
};

expect(resolveElicitationAvailability(classic, servingPath)).toStrictEqual({
formElicitation: false,
reason: 'serving_path',
});
});

test('gives client labels no authority over the outcome', () => {
const clientCapabilities = { elicitation: { url: {} } };
const labelled: Context = {
era: 'modern',
clientInfo: { name: 'claude-ai', version: '1.0.0' },
clientCapabilities,
};
const otherLabel: Context = {
era: 'modern',
clientInfo: { name: 'some-other-client', version: '9.9.9' },
clientCapabilities,
};

const known = resolveElicitationAvailability(labelled, servingPath);
const unknown = resolveElicitationAvailability(otherLabel, servingPath);

expect(known).toStrictEqual(unknown);
expect(known).toStrictEqual({
formElicitation: false,
reason: 'capability',
});
});
});
66 changes: 66 additions & 0 deletions packages/mcp-server-supabase/src/elicitations/capability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ToolRequestContext } from '@supabase/mcp-utils';

/**
* Whether this request can carry a form elicitation, and the one stable reason
* behind the answer.
*
* The reasons stay distinguishable so a caller can tell an operator opt-out
* from a serving path that cannot deliver a form, and both from a client that
* never declared form support.
*/
export type ElicitationAvailability = {
formElicitation: boolean;
reason: 'available' | 'serving_path' | 'opt_out' | 'capability';
};

/**
* Facts the entry point injects because capability metadata cannot derive
* them. Hosted URL parsing and route selection stay outside this package.
*/
export type ElicitationServingFacts = {
/** Whether the serving path in front of this server can deliver a form. */
formDeliveryAvailable: boolean;
/** Connection-level form elicitation opt-out. */
optOut?: boolean;
};

/**
* The SDK-owned facts the resolver reads. Client name and version are
* deliberately absent: no client label carries authority here, so there is no
* compatibility table to drift.
*/
type CapabilityContext = Pick<ToolRequestContext, 'era' | 'clientCapabilities'>;

/**
* Resolves form elicitation support from SDK-owned request facts combined with
* the injected serving-path facts.
*/
export function resolveElicitationAvailability(
ctx: CapabilityContext,
facts: ElicitationServingFacts
): ElicitationAvailability {
// A legacy request has no multi-round-trip leg to deliver a form on, so
// classic hosted and deprecated stdio stay incapable however they declare
// themselves.
if (!facts.formDeliveryAvailable || ctx.era !== 'modern') {
return { formElicitation: false, reason: 'serving_path' };
}

if (facts.optOut === true) {
return { formElicitation: false, reason: 'opt_out' };
}

const elicitation = ctx.clientCapabilities?.elicitation;
// A mode-less `elicitation: {}` predates the mode split and means every
// mode. A declaration that names its modes must name `form`, which leaves a
// URL-only declaration incapable.
const declaresForm =
elicitation !== undefined &&
(Object.keys(elicitation).length === 0 || 'form' in elicitation);

if (!declaresForm) {
return { formElicitation: false, reason: 'capability' };
}

return { formElicitation: true, reason: 'available' };
}
Loading
Loading